Skip to content

vulkan : fuse UNARY(GELU|SIGMOID|SILU|SOFTPLUS) + MUL - #27220

Open
Ankk98 wants to merge 6 commits into
ggml-org:masterfrom
Ankk98:vulkan-fuse-unary-mul
Open

vulkan : fuse UNARY(GELU|SIGMOID|SILU|SOFTPLUS) + MUL#27220
Ankk98 wants to merge 6 commits into
ggml-org:masterfrom
Ankk98:vulkan-fuse-unary-mul

Conversation

@Ankk98

@Ankk98 Ankk98 commented Aug 17, 2026

Copy link
Copy Markdown

Overview

Issue #27194

  • Fuses UNARY(GELU|SIGMOID|SILU|SOFTPLUS)+MUL on Vulkan, matching CUDA's unary_mul
  • GELU added per review for gemma4.

Additional information

Design:

  • Fusion code lives in unary.comp behind UNARY_MUL_FUSION ifdef; specialized pipelines per
    op/dtype, repeat + norepeat variants selected by shape equality.
  • Adjacent-only fuse via num_additional_fused_ops; gated by ggml_vk_can_fuse_unary_mul using
    the standard ggml_can_repeat(other, unary) broadcast check.
  • graph_optimize pulls the consuming MUL forward, and additionally hoists zero-compute nodes
    (NONE/VIEW/RESHAPE/TRANSPOSE/PERMUTE) out of UNARY→MUL gaps so view-separated sites fuse
    (per-layer embedding gating in gemma4/gemma3n builds this shape).
  • Kernel scheduled like mul.comp (256 threads x 2 iterations); writes MUL dst directly

Tests:

  • Fused ops: 96/96 (adds GELU_MUL, broadcast, non-1 repeat, and view-separated layouts)
  • 16935 -> 16949 tests, all backends OK
  • Plain ops: 123/123
  • Token-identical vs pre-PR base (4df29be) at temp 0/seed 42 on Qwen3-8B, Gemma-3n-E4B,
    Muse-Glimmer-30B, gemma4-12B.
  • Perf parity on Strix Halo after kernel-layout fix; note gemma4's gelu+mul site is view-separated, so it stays unfused (same as CUDA).
  • Hardware: AMD Radeon 8050S Graphics (RADV STRIX_HALO)
Updated results table (branch e3c1adce3 vs pre-PR base 4df29be4f, Strix Halo / RADV)
model (fused path)                   correctness vs pre-PR   pp2048
Qwen3-8B (none fire*)                token-identical         parity (723.8 vs 727.2, -0.5%)
Gemma-3n-E4B (GELU_MUL)              token-identical         +21.7% (1053.2 vs 865.0)
Muse-Glimmer-30B (SIGMOID_MUL)       token-identical         +16.0% (173.3 vs 149.4)
gemma-4-12B (none fire)              token-identical         ~parity, noisy (+7.3%, inert)


op suite                             16949/16949 OK          -


With tg256 detail:
model             tg256 PR            tg256 pre-PR        delta
Qwen3-8B          44.52 ± 0.09        44.60 ± 0.10        -0.2%
Gemma-3n-E4B      50.16 ± 0.17        44.74 ± 0.57        +12.1%
Muse-Glimmer-30B  12.96 ± 0.09        12.77 ± 0.53        +1.5%
gemma-4-12B       25.38 ± 2.63        24.40 ± 0.97        +4% (noise)

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES, used coding agent for debugging, coding and testing.

@Ankk98
Ankk98 requested a review from a team as a code owner August 17, 2026 05:20
@github-actions github-actions Bot added Vulkan Issues specific to the Vulkan backend ggml changes relating to the ggml tensor library for machine learning labels Aug 17, 2026
@ggml-gh-bot

ggml-gh-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

Hi @Ankk98, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • Multiple open PRs from a new contributor: We limit new contributors (those without a previously merged PR) to 1 open PR at a time. You currently have 2 open PRs.

Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@jeffbolznv

Copy link
Copy Markdown
Contributor

I'd like this to be done more consistently with existing fusions - add the code to the first shader in the sequence (unary), specialize the shaders (spec constant or ifdef) rather than runtime branching on push constants, and only fuse adjacent nodes (sort in graph_optimize only, don't try to find non-consecutive nodes at runtime, it won't work right).

- implement fusion in unary.comp behind UNARY_MUL_FUSION ifdef,
  specialized pipelines per op instead of runtime branching
- fuse adjacent nodes only, ordering handled by graph_optimize
- drop runtime consumer scan and pending_unary_mul deferral
Comment thread ggml/src/ggml-vulkan/vulkan-shaders/vulkan-shaders-gen.cpp
Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
});
}

static bool ggml_vk_should_fuse_unary_mul(const ggml_tensor * unary, const ggml_tensor * mul) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be grouped with the other ggml_vk_can_fuse functions (and should be called ggml_vk_can_fuse...)

Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
#undef CREATE_UNARY

#define CREATE_UNARY_MUL(name) \
ggml_vk_create_pipeline(device, device->pipeline_ ## name ## _mul[0], #name "_mul_f32", name ## _mul_f32_len, name ## _mul_f32_data, "main", 3, sizeof(vk_op_binary_push_constants), {512, 1, 1}, {1}, 1); \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These have norepeat=true, but seems like mul operations often use repeat. So I think we should support both.

Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
if (used[k]) {
continue;
}
if (std::find(current_set.begin(), current_set.end(), k) != current_set.end()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't follow all of this logic, and it's concerning that it's different from some existing cases. I wonder if this could just be a small new case alongside the group of reorderings that start with "// When we've found RMS_NORM + MUL, try to find a ROPE that uses it"

Ankk98 added 2 commits August 23, 2026 14:26
1. GELU: gelu_mul_f32/f16 pipelines registered, CREATE_UNARY_MUL(gelu), GELU in dispatch + fuse gate + perf fusion name
2. Renamed/moved: gate is now ggml_vk_can_fuse_unary_mul(cgraph, unary_idx, mul_idx), placed with the other can-fuse helpers
3. norepeat both variants: each op gets plain (spec {0}) + _norepeat (spec {1}) pipelines from the same SPIR-V, selected via ggml_are_same_shape(src0, src1); the shape gate now allows broadcast (other dims equal-or-1)
4. graph_optimize: lambda deleted; standard "// UNARY + MUL: pull the consuming MUL forward" block added alongside the SSM_CONV/ROPE/MUL_MAT reorderings, with the same "other src must be weights or already processed" readiness check
… test tolerance

- schedule the fused kernel like mul.comp (256 threads x 2 unrolled
  iterations), recovering a 10-18% prompt-processing regression
- allow 5e-7 f32 error for gelu_mul: the shader evaluates gelu with an
  exp-based tanh identity while the CPU reference uses tanhf (~1 ulp)
@Ankk98
Ankk98 requested a review from ggerganov as a code owner August 23, 2026 11:31
@github-actions github-actions Bot added the testing Everything test related label Aug 23, 2026
Comment thread ggml/src/ggml-vulkan/ggml-vulkan.cpp Outdated
if (!ggml_is_contiguous_1(other) || !ggml_is_contiguous_1(unary->src[0])) {
return false;
}
for (int i = 0; i < GGML_MAX_DIMS; ++i) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could just use ggml_can_repeat?

@Ankk98 Ankk98 changed the title vulkan : fuse UNARY(SIGMOID|SILU|SOFTPLUS) + MUL vulkan : fuse UNARY(GELU|SIGMOID|SILU|SOFTPLUS) + MUL Aug 25, 2026
Ankk98 added 2 commits August 25, 2026 07:59
The fused kernel indexes src1 via per-dim fastmod (generic_binary_head.glsl),
which is exact whenever the other operand tiles into the unary result -- not
just when its dims are equal or 1. Replace the hand-rolled loop with
ggml_can_repeat(other, unary) so the check matches the kernel's actual
capability and reuses the standard helper. Argument order matters: reversed,
it would wrongly admit graphs where the unary result is mul->src[1] and the
other operand is larger, producing truncated output.

Also add a rep_ne0 layout to the fused unary+mul backend tests covering a
non-1 repeat factor along dim 0.
gemma4's per-layer embedding gating builds gelu -> view_2d_slice -> mul,
where the intervening view is a zero-compute node aliasing an input that
was computed much earlier. Strict adjacency requirements meant neither
CUDA nor the vulkan unary+mul fusion handled this pattern.

Extend ggml_vk_graph_optimize to detect a UNARY whose consuming MUL is
separated only by unscheduled zero-compute nodes (GGML_OP_NONE, VIEW,
RESHAPE, TRANSPOSE, PERMUTE) and schedule those nodes ahead of the pair,
making it adjacent so the existing fusion applies. The reorder is guarded
by ggml_vk_can_fuse_unary_mul, a source-availability check for every
interleaved node, and the protected fusion patterns (topk_moe*, snake);
if fusion is later rejected the reordered graph still executes correctly,
just unfused.

Add a view_mid layout to the fused unary+mul backend tests replicating
the gemma4 pattern.
continue;
}

// UNARY + EMPTY* + MUL: when only zero-compute nodes (GGML_OP_NONE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block of code seems overcomplicated and unnecessary. The code at line 17836 should be able to pull the MUL ahead of a view.

get_indices(idx, i00, i01, i02, i03);

data_d[get_doffset() + dst_idx(i00, i01, i02, i03)] =
D_TYPE(FLOAT_TYPE(OP(float(data_a[get_aoffset() + src0_idx(i00, i01, i02, i03)]))) * FLOAT_TYPE(data_b[get_boffset() + src1_idx(i00, i01, i02, i03)]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also support running the OP on the B operand? I think this happens in qwen models:

        // Apply sigmoid to the gate
        shared_gate = ggml_sigmoid(ctx0, shared_gate);
        cb(shared_gate, "shared_expert_gate_sigmoid", il);


        // Apply the gate to the shared expert output
        ffn_shexp = ggml_mul(ctx0, ffn_shexp, shared_gate);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ggml changes relating to the ggml tensor library for machine learning testing Everything test related Vulkan Issues specific to the Vulkan backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants