Skip to content

[CPU] QMoE: float zero-points for fractional/asymmetric quantization - #32199

Draft
Thiago Pereira Rocha (thpereir) wants to merge 3 commits into
microsoft:mainfrom
thpereir:float-zp-cpu-qmoe-upstream
Draft

[CPU] QMoE: float zero-points for fractional/asymmetric quantization#32199
Thiago Pereira Rocha (thpereir) wants to merge 3 commits into
microsoft:mainfrom
thpereir:float-zp-cpu-qmoe-upstream

Conversation

@thpereir

Copy link
Copy Markdown

Summary

Adds float zero-point support to the CPU QMoE kernel, enabling fractional/asymmetric quantization schemes that cannot be represented on the integer code grid. Motivating case: the Quark uint2 gemma-4-26B-A4B-it export, whose zero-points are a constant 1.5 (codes {0,1,2,3}{-1.5,-0.5,0.5,1.5} * scale).

Stacked on #32153 (GeGLU CPU QMoE). This PR's own commit is a clean 1-commit delta on top of that branch; the diff here will narrow to just the float-zp change once #32153 merges and this rebases onto main.

What changes

  • contrib_defs.cc: new TZ type constraint so fc*_zero_points accept float/float16/bfloat16 in addition to packed uint8; schema docs describe the integer (T1) vs float (TZ) layouts.
  • moe_quantization_cpu.cc: detect float zp by element type, read it as a parallel unpacked one-per-group pointer (same layout as scales), gate off the integer-only fast paths (LUT GEMM, prepack, direct Q4), and dequantize as w = (code - zero_point) * scale.
  • moe_helper.h: per-tensor zp packing factor so unpacked float zp passes shape validation (pack factor 1 instead of 8/bits).

Integer zero-points are unchanged — float zp is detected purely by dtype and only alters the block-wise dequant branch.

Test plan

  • CPU QMoE parity tests pass (integer int4/int8 unaffected)
  • Fractional-zp (uint2 zp=1.5) Gemma4 MoE export produces correct outputs via the dequantize→MlasGemm path

Gemma-style MoE experts use GeGLU (gelu_pytorch_tanh(gate) * up), which the
fused CPU QMoE op could not express: its only gated path was SwiGLU
(silu-gated), and plain gelu is non-gated (single-width fc1). Add a
first-class geglu activation that reuses the interleaved doubled-fc1 SwiGLU
machinery but applies the gelu-tanh gate. CPU QMoE only; CUDA and float MoE
paths are unchanged.

- moe_base_cpu.h: ActivationType::GeGLU + "geglu" parser branch
- moe_utils.{h,cc}: ApplyGeGLUActivation (tanh-gelu gate, matches HF
  gelu_pytorch_tanh)
- moe_quantization_cpu.cc: treat GeGLU as a gated activation (doubled fc1,
  swiglu_fusion=1) and dispatch to ApplyGeGLUActivation
Mirror the existing SwiGLU QMoE CPU parity suite for the new GeGLU activation:
geglu() reference (gelu-tanh gate matching ApplyGeGLUActivation), GegluMlp /
GegluMoeConfig / GegluMoEBlock, and TestGegluQMoECPU covering row-wise and
block-wise, symmetric and asymmetric, at 2/4/8-bit. The shared ONNX graph
builder now emits activation_type="geglu" with a plain gelu gate
(alpha=1, beta=0, no clamp), distinct from SwiGLU's scaled/clamped gate.
Enables the CPU QMoE kernel to run the fused Quark uint2 gemma-4-26B-A4B-it
export, whose zero-points are float (a constant 1.5 -> codes {0,1,2,3} map to
{-1.5,-0.5,0.5,1.5}*scale), unrepresentable on the integer code grid.

Float zero-points (this change):
- contrib_defs.cc: new TZ type constraint so fc*_zero_points accept float/
  float16/bfloat16 in addition to packed uint8
- moe_quantization_cpu.cc: detect float zp by dtype, read it as a parallel
  unpacked one-per-group pointer (scales layout), gate off the integer-only
  fast paths (LUT GEMM, prepack, direct Q4), and dequantize as (code-zp)*scale
- moe_helper.h: per-tensor zp packing factor so unpacked float zp passes shape
  validation (pack factor 1 instead of 8/bits)

GeGLU CPU QMoE support (from the geglu-qmoe-cpu baseline; the worktree base
commit predates it): the ORT_ENFORCE now accepts GeGLU as well as SwiGLU with
swiglu_fusion=1, and the gated-activation path dispatches to
ApplyGeGLUActivation for the geglu activation type.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

)
# Swap SwiGLU experts for GeGLU experts and flip the activation flags. The graph is
# (re)built lazily in recreate_onnx_model(), which reads self.use_geglu.
self.use_swiglu = False
# (re)built lazily in recreate_onnx_model(), which reads self.use_geglu.
self.use_swiglu = False
self.use_geglu = True
self.experts = nn.ModuleList([GegluMlp(config) for _ in range(self.num_experts)])

Copilot AI left a comment

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.

Pull request overview

Adds fractional float zero-points to CPU QMoE, alongside stacked GeGLU changes from #32153.

Changes:

  • Extends QMoE zero-point schema types and shape validation.
  • Adds float zero-point dequantization routing.
  • Adds CPU GeGLU implementation and parity tests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
contrib_defs.cc Extends QMoE zero-point schema types.
moe_base_cpu.h Adds GeGLU activation parsing.
moe_helper.h Validates unpacked float zero-point shapes.
moe_quantization_cpu.cc Implements float zero-point and GeGLU dispatch.
moe_utils.h Declares GeGLU helper.
moe_utils.cc Implements GeGLU activation.
test_qmoe_cpu.py Adds GeGLU parity coverage.
Suppressed comments (1)

onnxruntime/contrib_ops/cpu/moe/moe_quantization_cpu.cc:1225

  • No test in this PR supplies a non-uint8 zero-point: create_cpu_moe_onnx_graph still unconditionally casts both zero-point tensors to numpy.uint8 and emits TensorProto.UINT8 (test_qmoe_cpu.py:594-617). Thus the new branch and the motivating fractional value 1.5 are never exercised, which leaves the unsupported 4-bit, 8-bit, and row-wise paths undetected. Add fractional float-zero-point parity tests covering the intended bit widths/layouts and fast-path settings.
  const bool fc1_zp_is_float = fc1_zero_points != nullptr &&
                               fc1_zero_points->template IsDataType<T>();
  const bool fc2_zp_is_float = fc2_zero_points != nullptr &&
                               fc2_zero_points->template IsDataType<T>();
  const uint8_t* fc1_zp_data = (fc1_zero_points && !fc1_zp_is_float) ? fc1_zero_points->template Data<uint8_t>() : nullptr;
  const uint8_t* fc2_zp_data = (fc2_zero_points && !fc2_zp_is_float) ? fc2_zero_points->template Data<uint8_t>() : nullptr;
  const T* fc1_zp_fp_data = fc1_zp_is_float ? fc1_zero_points->template Data<T>() : nullptr;
  const T* fc2_zp_fp_data = fc2_zp_is_float ? fc2_zero_points->template Data<T>() : nullptr;

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +46 to +47
} else if (activation_type_str == "geglu") {
activation_type_ = ActivationType::GeGLU;
Comment on lines +313 to +318
const TScale* zero_points_fp) {
// zero_points_fp (optional): float zero-points laid out identically to `scales`
// (unpacked, one value per group, same index). When present it takes precedence over the
// packed integer `zero_points` and dequant is w = (code - zero_point) * scale. This supports
// fractional/asymmetric schemes (e.g. Quark uint2 with a constant zp of 1.5) that cannot be
// represented on the integer code grid. Only wired for the 2/4-bit block-wise branch.
Comment on lines +1673 to +1675
.TypeConstraint("TZ", {"tensor(uint8)", "tensor(float8e4m3fn)", "tensor(float)", "tensor(float16)", "tensor(bfloat16)"},
"Constrain zero-point types. Integer zero-points use packed uint8 (same layout as weights). "
"Float zero-points are unpacked (one per group, scales layout) and support fractional/asymmetric schemes.")
Comment on lines +1604 to +1608
"3D tensor with shape (num_experts, fusion_size * inter_size, hidden_size / block_size / pack_size) when block_size is provided. "
"Integer zero-points use the same packed uint8 layout as the weights (T1). Float zero-points "
"(TZ) are unpacked, one value per group, matching the scales layout, and dequantize as "
"w = (code - zero_point) * scale.",
"TZ",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants