metal: fuse snake activation (mul, sin, sqr, mul, add)#25459
metal: fuse snake activation (mul, sin, sqr, mul, add)#25459ServeurpersoCom wants to merge 3 commits into
Conversation
Mirror the CUDA, Vulkan and CPU snake fusion: same matcher on the naive 5-op chain, same F32 contract on a and inv_b, same F32/F16/BF16 kernel with F32 compute. Follows the Metal backend idioms: bf16 instantiation gated behind GGML_METAL_HAS_BF16 and concurrency ranges checked on the remaining chain nodes before encoding, as done by the bin fusion. Covered by the existing backend-agnostic SNAKE_FUSE tests.
|
F32 looks good, but the kernel_snake_f16/bf16 are never dispatched. Binary Metal ops currently require src0 == F32 in ggml_metal_device_supports_op. the head of the Snake fusion chain mul(x, a) will fall back to CPU, and the 5-op fusion chain never materializes on Metal. |
Right, the leading mul is gated to F32 by supports_op, so the f16/bf16 variants never dispatch today. The kernel body is a single template shared with the F32 path, so this is just future proofing, and every backend carries such variants: f16/bf16 on Vulkan behind the SIN/SQR gate (#22855), bf16 on CUDA behind the bin_bcast filter (#22912). They light up when the missing ops are added. |
|
Snake fusion, fused chain and missing ops inventory : PRs to do (this would double performance for vocoders) :
|
|
Got it, I'm ok keeping the dormant f16/bf16 variants here for now. |
|
I’ll do some follow-up PRs, starting tests with my GGML fork, to get real benchmarks and user feedback on my small *.cpp projects. |
| // Snake activation autofuse: mul -> sin -> sqr -> mul -> add | ||
| bool fused = false; | ||
| if (node->op == GGML_OP_MUL && ctx->use_fusion) { | ||
| static constexpr ggml_op snake_ops[5] = { GGML_OP_MUL, GGML_OP_SIN, GGML_OP_SQR, GGML_OP_MUL, GGML_OP_ADD }; | ||
| if (ctx->can_fuse(idx, snake_ops, 5)) { | ||
| const ggml_tensor * mul0 = ctx->node(idx + 0); | ||
| const ggml_tensor * sin_node = ctx->node(idx + 1); | ||
| const ggml_tensor * sqr = ctx->node(idx + 2); | ||
| const ggml_tensor * mul1 = ctx->node(idx + 3); | ||
| const ggml_tensor * add = ctx->node(idx + 4); | ||
|
|
||
| // x carries the full activation shape, a is the broadcast operand | ||
| const ggml_tensor * x = ggml_are_same_shape(mul0, mul0->src[0]) ? mul0->src[0] : mul0->src[1]; | ||
| const ggml_tensor * a = (x == mul0->src[0]) ? mul0->src[1] : mul0->src[0]; | ||
|
|
||
| // mul1 reads sqr and inv_b in either operand order | ||
| const ggml_tensor * inv_b = (mul1->src[0] == sqr) ? mul1->src[1] : mul1->src[0]; | ||
|
|
||
| // closure check: the trailing add reads the same x as the leading mul | ||
| const ggml_tensor * x_in_add = (add->src[0] == mul1) ? add->src[1] : add->src[0]; | ||
|
|
||
| // x is in the supported whitelist and every chain intermediate shares x's type. | ||
| // a and inv_b bind as device const float * in the kernel, so they stay F32. | ||
| const bool types_ok = | ||
| (x->type == GGML_TYPE_F32 || x->type == GGML_TYPE_F16 || x->type == GGML_TYPE_BF16) && | ||
| (a->type == GGML_TYPE_F32) && (inv_b->type == GGML_TYPE_F32) && | ||
| (mul0->type == x->type) && (sin_node->type == x->type) && | ||
| (sqr->type == x->type) && (mul1->type == x->type) && | ||
| (add->type == x->type); | ||
| // a / inv_b collapse to [1, C, 1, 1], x and add stay 2D | ||
| const bool shape_ok = ggml_are_same_shape(a, inv_b) && a->ne[0] == 1 && a->ne[1] == x->ne[1]; | ||
| const bool dim_ok = | ||
| (x->ne[2] == 1) && (x->ne[3] == 1) && | ||
| (add->ne[2] == 1) && (add->ne[3] == 1) && | ||
| (a->ne[2] == 1) && (a->ne[3] == 1) && | ||
| (inv_b->ne[2] == 1) && (inv_b->ne[3] == 1); | ||
| // kernel reads x[idx] and a[c] / inv_b[c] linearly, so every operand is contiguous | ||
| const bool contig_ok = | ||
| ggml_is_contiguous(x) && ggml_is_contiguous(add) && | ||
| ggml_is_contiguous(a) && ggml_is_contiguous(inv_b); | ||
| if (types_ok && shape_ok && dim_ok && contig_ok && x_in_add == x) { | ||
| n_fuse = ggml_metal_op_snake_fused(ctx, idx); | ||
| fused = true; | ||
| } | ||
| } | ||
| } | ||
| if (!fused) { | ||
| n_fuse = ggml_metal_op_bin(ctx, idx); | ||
| } |
There was a problem hiding this comment.
This logic should be absorbed inside the ggml_metal_op_bin(). We want to keep this switch statement clean.
There was a problem hiding this comment.
Done, matcher extracted to ggml_metal_op_can_fuse_snake and dispatch absorbed into ggml_metal_op_bin, the switch is back to a single call.
Extract the matcher to ggml_metal_op_can_fuse_snake, mirroring the Vulkan naming, and dispatch the fused path from ggml_metal_op_bin. The encode loop switch is back to a single call per case. Address review from @ggerganov
| // closure check: the trailing add reads the same x as the leading mul | ||
| const ggml_tensor * x_in_add = (add->src[0] == mul1) ? add->src[1] : add->src[0]; | ||
|
|
||
| // x is in the supported whitelist and every chain intermediate shares x's type. | ||
| // a and inv_b bind as device const float * in the kernel, so they stay F32. | ||
| const bool types_ok = | ||
| (x->type == GGML_TYPE_F32 || x->type == GGML_TYPE_F16 || x->type == GGML_TYPE_BF16) && | ||
| (a->type == GGML_TYPE_F32) && (inv_b->type == GGML_TYPE_F32) && | ||
| (mul0->type == x->type) && (sin_node->type == x->type) && | ||
| (sqr->type == x->type) && (mul1->type == x->type) && | ||
| (add->type == x->type); | ||
| // a / inv_b collapse to [1, C, 1, 1], x and add stay 2D | ||
| const bool shape_ok = ggml_are_same_shape(a, inv_b) && a->ne[0] == 1 && a->ne[1] == x->ne[1]; | ||
| const bool dim_ok = | ||
| (x->ne[2] == 1) && (x->ne[3] == 1) && | ||
| (add->ne[2] == 1) && (add->ne[3] == 1) && | ||
| (a->ne[2] == 1) && (a->ne[3] == 1) && | ||
| (inv_b->ne[2] == 1) && (inv_b->ne[3] == 1); | ||
| // kernel reads x[idx] and a[c] / inv_b[c] linearly, so every operand is contiguous | ||
| const bool contig_ok = | ||
| ggml_is_contiguous(x) && ggml_is_contiguous(add) && | ||
| ggml_is_contiguous(a) && ggml_is_contiguous(inv_b); | ||
|
|
||
| return types_ok && shape_ok && dim_ok && contig_ok && x_in_add == x; |
There was a problem hiding this comment.
Indentation is wrong in this section.
Overview
Metal counterpart of #22667 (CUDA) and #22855 (Vulkan): same matcher on the naive 5-op chain (mul, sin, sqr, mul, add), same broadcast contract (a / inv_b in F32, shaped [1, C] over x [T, C]), single kernel templated on x's type with F32 compute.
Follows the Metal backend conventions: the bf16 instantiation is gated behind GGML_METAL_HAS_BF16, and the fused dispatch checks concurrency ranges on the remaining chain nodes before encoding, as the bin fusion does, since the pre-check in the encode loop only covers the leading mul.
Covered by the existing backend-agnostic SNAKE_FUSE tests from #22667.
Additional information
Requirements