Repository navigation
[GPU] Support u2 in the fused MoE 3-GEMM path - #39062
Cyberpunk1210 wants to merge 14 commits into
Conversation
Prepares MoeOpFusion for INT2 (u2) MoE models, and stops it fusing layers whose
expert GEMMs mix dtypes, which the fused kernel decodes wrongly today.
u2 itself stays inert until the GPU kernels land in a follow-up PR: the only
caller is the GPU pipeline, and it does not yet offer u2 to
ConvertTiledMoeBlockToGatherMatmuls / ConvertGatherMatmulToGatherMatmulCompressed,
so no u2 GatherMatmulCompressed reaches this pass.
Changes to the compressed branch of the 3-GEMM callback:
- Weight dtype gate. Previously any combination fused, but moe_3gemm decodes
all three GEMMs with the gate GEMM's dtype, so a layer with e.g. u4 gate/up
and u8 down (NNCF mixed precision keeps sensitive experts at INT8) produced
wrong output with no error. A layer now fuses only when its three GEMMs share
one dtype in {u2, u4, i4, u8, i8}, or mix only u2 and u8 -- the one mix the
follow-up kernels decode per GEMM. Other mixes stay in the GatherMatmul form,
which is correct.
- Scalar zero point. A rank-0 zp constant is a folded per-tensor zero point.
An all-zero one is normalized to "no zp" (symmetric); a non-zero one is kept
only for u2 GEMMs, and op::MOE validation now skips the num_experts check for
rank-0 inputs. Mixing symmetric and asymmetric GEMMs in one layer bails out.
- Group size. MOECompressed needs one group size for all three GEMMs, so a
per-channel scale (and its u8 zp) is broadcast to the group-wise layout of the
others; genuinely different group sizes bail out. Sub-byte zps are not
expanded -- they are bit-packed and Type::size() rounds up to a byte -- so
that case declines to fuse.
- The 2-GEMM matcher applies the same scalar-zp normalization and keeps u2 in
GatherMatmul form, since moe_gemm's only implementation (oneDNN) has no u2.
Tests: compressed GatherMatmulCompressed variants of the MoE block -- until now
every model in this file used plain f32 GatherMatmul, so the whole compressed
half of the callback was untested -- covering the dtype admission table, the
scalar-zp normalization, group-size unification and the per-channel scale/zp
broadcast.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
The moe_3gemm decode kernels took one weight dtype from the gate GEMM (WEIGHT_COMPRESSEION_DT) and derived from it the GEMV variant, the per-expert weight stride and the per-expert zp stride for all three GEMMs. A layer whose GEMMs differ -- NNCF mixed precision produces e.g. gate/up u8 with down u4 -- therefore decoded at least one GEMM with the wrong dtype and the wrong stride: wrong output, no error. Emit GATE/UP/DOWN_WEIGHT_DT and use them for the GEMV dispatch, the expert weight stride and the zp stride of each GEMM. The activation interleave stays keyed on one dtype per kernel: gate and up share one SLM copy whose layout is picked from the gate dtype, so they must still agree on whether they are u4; down stages its own copy and is free. The prefill paths already took each GEMM's dtype. Also fix the host-side GEMV check: FAKE_GROUP_SIZE in the kernel is the minimum of the gate/up and the down group size, but the assert only looked at gate/up, so a smaller down group slipped through to an ELEMS_PER_LANE with no kernel branch. Tests: moe_3gemm_compressed_gpu_mixed_dtype, gate/up u8 + down u4 and gate/up u4 + down u8, for single-token decode, batched GEMV and just above the batched-GEMV threshold, against a reference built from the exact dequantized weights. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
Brings the fused moe_3gemm_swiglu path to u2 (INT2) expert weights and enables u2 for the GPU MoE passes. Depends on "[Transformations] Support u2 expert weights in MoE op fusion" and on "[GPU] Decode each GEMM of the fused MoE 3-GEMM with its own weight dtype". Prefill runs u2 through the existing oneDNN grouped GEMM by unpacking it to u4; a native u2 prefill GEMM follows separately. Decode (GEMV and batched GEMV). u2 GEMV variants of gate_up and down, dispatched per GEMM as dtype code 3. They read activations from SLM so that lane L's activations line up with the contiguous K values packed in its weight byte (w[4L..4L+3]); the u4 path's strided read targets an interleaved layout and is left unchanged. Per-group u2 zero points are decoded in-kernel; a scalar zero point (INT2_SYM) is broadcast, and whether a GEMM's zp is scalar is answered per GEMM (GATE/UP/DOWN_ZP_SCALAR), since a mixed INT2_SYM/INT2_ASYM layer gives gate and up different forms. Only the u2 variants have an ELEMS_PER_LANE == 1 branch, so the smaller group size is admitted only when all three GEMMs are u2. Prefill. oneDNN and the micro-GEMM have no u2 type, so the u2 expert weights (and zp) are unpacked once into u4 scratch and the u4 grouped GEMM consumes them. The unpack runs once per bound weight buffer rather than per prefill -- the weights are constant, since expert offload is rejected for u2 -- which removed ~430 ms of TTFT per prefill on Qwen3.6-35B-A3B. Shared experts. A u2 shared projection is reported to oneDNN as u4, so it is unpacked the same way; the unpack is per projection because NNCF mixed precision makes any subset of the projections u2. Mixed precision. MoE op fusion now also admits u4 in a mixed layer, as long as gate and up agree on whether they are u4 (they share one SLM activation copy whose layout the gate dtype picks). This admits the NNCF recipe that keeps gate/up at u2 and gives down u4. It lands here rather than with the fusion change because only now does every admitted combination decode per GEMM. Pipeline: u2 is offered to ConvertTiledMoeBlockToGatherMatmuls and ConvertGatherMatmulToGatherMatmulCompressed only when MoE fusion is enabled -- GatherMatmulCompressed has no u2 kernel, and those passes now also run under OV_GPU_MOE_DISABLE_FUSION -- and never to ConvertGroupedMatMulToGroupedMatMulCompressed (oneDNN-only). FuseGatedMLP and KeepMOE3GemmConstPrecision accept u2. Tests: moe_3gemm u2 cases for decode GEMV, batched GEMV and prefill, scalar and per-group zero points, mixed u2/u8 and u2/u4 layers, and the u2 shared-expert unpack; the MoE op fusion dtype table for the u4 mixes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
A failed u2 weight unpack or grouped GEMM used to be caught and silently replaced by the batched GEMV. No other GPU primitive degrades that way at execution time, so let the error propagate. Also drop measurement figures from comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
Comment-only cleanup of the u2 fused MoE 3-GEMM changes and tests: keep the why, drop restated code and history. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
Comment-only cleanup: keep the why, drop restated code, history and internal labels. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
Comment-only cleanup: keep the why, drop restated code and history. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
|
build_jenkins |
Drops an unneeded lambda capture (clang -Wunused-lambda-capture). No behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
|
build_jenkins |
…ith folded ops - Each compressed expert weight's dtype is now a pattern predicate (type_matches_any in the 3-GEMM matcher, "not u2" in the 2-GEMM matcher); only the cross-GEMM rule (uniform, or all mixable with gate/up agreeing on u4) stays in the callback. - Per-channel scale/zp expansion uses Reshape + Broadcast with make_try_fold instead of a raw byte copy. - The scalar-zp normalization is shared by both matchers and takes whether a non-zero zp may pass; the 2-GEMM matcher never admits one, which the old comment misstated. No change in what fuses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
…op the broadcast wrapper lambda Same behaviour: non-Constant zp still passes unless rank-0; the wrapper lambda only forwarded group_size. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
…e WEIGHT_COMPRESSEION_DT validate_impl now checks WEIGHT/SCALE/ZP of gate, up and down, plus the cross-input constraints the kernel already relies on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
moe_3gemm_swiglu_opt.cpp: took upstream's gate GEMM fused with the up output as a binary post-op, and routed its weight and zp through grouped_weight_mem/grouped_zp_mem so u2 gate weights still come from the unpacked u4 scratch. fuse_gated_mlp.cpp: deleted, as upstream removed FuseGatedMLP (openvinotoolkit#39202); our u2 additions to it had no remaining callers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
Prefill runs the shared expert through oneDNN with per-projection dtypes; validate_impl cannot tell decode from prefill, so the check rejected valid models. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
src/common/transformations/src/transformations/common_optimizations/moe_op_fusion.cpp, tests/.../convert_gather_matmuls_moe_block_to_moe_op.cpp, src/core/src/op/moe.cpp: same resolution as int2-moe-fusion (one zp path for u2/u3, u3 in the weight list, upstream moe.cpp), plus this branch's mixed u2/u4/u8 gate/up rule re-applied on top and kept; test table keeps both u3 and our u2 rows. src/plugins/intel_gpu/src/graph/impls/ocl_v2/utils/jitter.cpp: kept u2 and u3. src/plugins/intel_gpu/src/plugin/transformations_pipeline.cpp: u3 added to supported_compressed_weights_types; our gather_matmul_weights_types (that list + u2 when fusion is on) is derived from it, so u3 reaches the GatherMatmul passes too. src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_3gemm_swiglu_opt.hpp: u2 and u3 both supported; the "zp bit width == weight bit width" check (batched GEMV zp stride, non-scalar zps only) exempts u3. src/plugins/intel_gpu/src/graph/impls/ocl_v2/moe/moe_3gemm_swiglu_opt.cpp: ported onto MoeGemmImpl. u2 is batched-GEMV capable (decode_impl batched_gemv); any u2 GEMM forces prefill_impl grouped_gemm. The u2->u4 unpack of routed weights/zp runs in exec_grouped_gemm_core (both mask gen entry points); shared-expert unpack stays in execute() for every prefill path. exec_gemm and its args-cache key use the unpacked buffers for u2 GEMMs. The grouped GEMM zp dtype is now per GEMM (u4 for an unpacked u2 zp). A scalar zp stays on our broadcast-into-u4 unpack for u2 GEMMs: upstream host-scalar zp (u3 only) cannot create a oneDNN matmul with u4 weights, so u2 never uses _grouped_zp_host_scalars. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Cyberpunk1210 <genji.1231@outlook.com>
| @@ -44,7 +44,11 @@ struct Moe3GemmReference { | |||
| std::vector<uint8_t> pack(const std::vector<uint8_t>& unpacked) { | |||
There was a problem hiding this comment.
Please add functional tests to src/plugins/intel_gpu/tests/functional/subgraph_tests/dynamic/moe.cpp for the u2 case, covering both 3-GEMM and 2-GEMM scenarios (to ensure that the pipeline does not crash, but instead falls back to decomposition)
| // The shared-expert oneDNN primitives consume their u4 scratch on every prefill path. The routed experts' unpack | ||
| // lives in exec_grouped_gemm_core, shared by both mask gen entry points. | ||
| if (has_u2_shared_weights()) { | ||
| unpack_u2_shared_weights_for_prefill(instance, scratch, events); |
There was a problem hiding this comment.
The returned event is not handled in any way
| MoeU2SharedParams{false, false, false, 64, 64, "none_u2_prefill"}, | ||
| MoeU2SharedParams{true, false, false, 64, 64, "gate_u2_prefill"}, | ||
| MoeU2SharedParams{false, false, true, 64, 64, "down_u2_prefill"}, | ||
| MoeU2SharedParams{true, false, true, 64, 64, "gate_down_u2_prefill"}), |
There was a problem hiding this comment.
And what about the decode?
v-Golubev
left a comment
There was a problem hiding this comment.
A question on the overall direction. oneDNN already supports u2 FC (#37212), and the oneDNN path is preferable: once it lands, prefill needs no custom code on our side, which means much less to maintain. So maybe a better idea is to contribute to u2 grouped GEMM support in OneDNN instead of a custom kernel on our side.
Moreover, the fused MoE path is enabled only for IMMAD + oneDNN (see the gate in transformations_pipeline.cpp), so non-IMMAD devices never reach these kernels. So there is no argument on non-IMMAD devices support in MoE node.
Could you please clarify which code paths this PR closes that oneDNN grouped GEMM wouldn't?
| constexpr size_t kE = 2; | ||
| constexpr size_t kHidden = 16; |
There was a problem hiding this comment.
| constexpr size_t kE = 2; | |
| constexpr size_t kHidden = 16; | |
| constexpr size_t k_e = 2; | |
| constexpr size_t k_hidden = 16; |
Detail: in common part this naming convention should be used.
Details:
Part 6 of the split of #37335 (series: #39057 … #39063). Depends on #39060 and #39061; this branch
contains both commits, so until they merge please review only the last commit. The native u2 prefill
GEMM is the follow-up #39063.
Brings
moe_3gemm_swigluto u2 expert weights and enables u2 for the GPU MoE passes.loads aligned to the contiguous K values in each lane's weight byte. Per-group u2 zero points are
decoded in-kernel; a scalar zp (INT2_SYM) is broadcast, and whether a GEMM's zp is scalar is
answered per GEMM (
GATE/UP/DOWN_ZP_SCALAR), since a mixed INT2_SYM/INT2_ASYM layer gives gate andup different forms. Only the u2 variants have an
ELEMS_PER_LANE== 1 branch, so the smaller groupsize is admitted only when all three GEMMs are u2.
oneDNN grouped GEMM. Functional, but the scratch is twice the size of the u2 expert weights; [GPU] Add a native u2 prefill GEMM to the fused MoE 3-GEMM path #39063
removes it.
on whether they are u4. This admits the NNCF recipe that keeps gate/up at u2 and gives down u4.
ConvertTiledMoeBlockToGatherMatmulsandConvertGatherMatmulToGatherMatmulCompressedonly when MoE fusion is enabled — GatherMatmulCompressedhas no u2 kernel, and those passes now also run under
OV_GPU_MOE_DISABLE_FUSION— and never to theoneDNN-only GroupedMatMul pass;
FuseGatedMLPandKeepMOE3GemmConstPrecisionaccept u2.Tests:
moe_3gemmu2 cases for decode GEMV, batched GEMV and prefill, scalar and per-group zeropoints, mixed u2/u8 and u2/u4 layers, and the u2 shared-expert unpack; the MoE op fusion dtype table
for the u4 mixes.
Known:
moe_accuracy_test_random/10fails on master itself (see #39061) and is unchanged here.Tickets:
AI Assistance:
close supervision.
*moe*unit tests (218/219,the one failure being the pre-existing case above) and transformation tests (33/33).
🤖 Generated with Claude Code