Skip to content

[follow-up #168] wrap_qmoe hard-codes zp_elem_size=1; should detect from zero_points dtype #176

Description

@BoarQing

Summary

wrap_qmoe hard-codes zp_elem_size = 1 (uint8 packed nibbles) for both FC1 and FC2 calls into hip_matmul_nbits. This silently mis-unpacks zero_points if a future QMoE export emits fp16 zero_points (which wrap_matmul_nbits already supports via zp_elem_size = 2, added in #162).

Where

HIP_CHECK(hip_matmul_nbits(stream, d_gather_buf, fc1_w_e, fc1_s_e,
                           fc1_zp_e, fc1_b_e, d_fc1_buf, count,
                           fusion_inter, hidden_size, 1,
                           expert_weight_bits, block_size, elem_size,
                           /*zp_elem_size=*/1));

Why it matters

Today every QMoE model on disk that we run uses uint8 packed-nibble zero_points (the gpt-oss-120b-AWQ family is the motivating one), so this works in practice. But:

  1. The MS QMoE spec permits fp16 zero_points (same dtype as scales) in addition to packed uint8.
  2. wrap_matmul_nbits already differentiates these via zp_elem_size (1 = byte/packed-nibble, 2 = fp16) — see fix: support uint8 packed nibble zero_points in MatMulNBits WMMA/GEMV paths #162.
  3. The plumbing for QMoE is not symmetric: there is no place in wrap_qmoe that inspects the ONNX *_zero_points tensor's element type, so even if a model exported fp16 ZPs they would be reinterpreted as packed bytes and produce silent garbage outputs (no assertion, no diagnostic).

The recently-fixed packed-nibble per-expert stride in qmoe.cpp (PR #168, commit 0918fdf) also assumes zp_elem_size == 1 (the stride is e * N * ((k_blocks + 1) / 2)); a generic fix would need to compute the stride as e * N * k_blocks * zp_elem_size_in_bytes with the correct unpacking for the byte-element case.

Suggested fix

wrap_qmoe already takes void *fc1_zero_points / void *fc2_zero_points as raw pointers, so the dtype information is lost by the time control reaches the runtime. The fix lives one level up:

  1. Plumb the zero_points dtype through the wrapper signature. Either add an int64_t zp_elem_size parameter to wrap_qmoe (mirroring wrap_matmul_nbits), or split into fc1_zp_elem_size / fc2_zp_elem_size for completeness.
  2. At lowering time (lib/Conversion/HipToLLVM/QMoEToLLVM.cpp or wherever wrap_qmoe is called), inspect the element type of the fc{1,2}_zero_points operand: ui81, f162. Pass that constant down.
  3. In wrap_qmoe, replace the hard-coded 1 at lines 369 and 399 with the per-FC parameter.
  4. Update the per-expert stride computation to use e * N * ((k_blocks + 1) / 2) only when zp_elem_size == 1, and e * N * k_blocks * 2 when zp_elem_size == 2 (no packing for fp16).
  5. Add lit coverage: extend test/lit/Conversion/onnx-to-hip/test_qmoe_packed_zp.mlir (added in fix(gemm,qmoe,gqa,matmul): unblock gpt-oss-120b end-to-end (4 fixes) #168) with an f16 ZP variant and CHECK that the lowering picks up zp_elem_size = 2.

Out of scope for #168

This issue is not a regression introduced by #168 - it is pre-existing behaviour surfaced during the final review. The packed-nibble stride fix in #168 is correct for every model we currently care about. The fix above is appropriate as a follow-up so that QMoE achieves full parity with MatMulNBits re: optional ZP dtype.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions