fix(gbq): key GatherBlockQuantized default zero point off ONNX storage type, not signedness - #609
Draft
BoarQing wants to merge 1 commit into
Draft
fix(gbq): key GatherBlockQuantized default zero point off ONNX storage type, not signedness#609BoarQing wants to merge 1 commit into
BoarQing wants to merge 1 commit into
Conversation
|
Thanks for opening a PR! This project follows LLVM's incremental-development and AI-tool-use Before requesting review, please check that:
Reviewers are assigned through |
L2 Accuracy Results (EP vs CPU)
Threshold: 0.01 | Run: 3486 - Commit: |
MorphiZen EP Performance Results
EPContext Export Performance
EPContext Import Performance
OGA Benchmark Results
OGA Wheel Smoke (Python benchmark_e2e.py)
Run: 3486 - Commit: |
BoarQing
force-pushed
the
fix/gbq-storage-type
branch
from
July 31, 2026 09:29
676d2ef to
4b259d8
Compare
GatherBlockQuantized picks its implicit zero point from the T1 storage type, not from whether that type is unsigned. The ORT CPU kernel branches on the storage type alone: uint8 uses 2^(bits-1) and both int4 and uint4 use 0, so uint4 defaults to 0 despite being unsigned. Selecting on signedness biases every uint4 embedding without explicit zero points by +(8 * scale). uint4 and uint8-holding-packed-nibbles are byte-identical in MLIR -- both arrive as ui8 tensors with the same `bits` -- so the distinction has to be carried explicitly. The importer records the ONNX element type on each constant, the conversion turns that into a `quant_storage_bits` attribute declared in ODS with a verifier, and the lowering forwards it to the runtime as a new trailing argument. Measured against ORT CPU on the single-op variants, uint4 without zero points goes from L2 166.277 to 0 (and 940.604 to 0 on the 100352x2048 version). uint8 with bits=4 stays at 0, confirming the rule keys off storage width rather than `bits`: that case still needs a zero point of 8. Tests cover each layer the width travels through: the element-type mark selecting 8 for uint8 and 4 for uint4/int4 on otherwise identical IR, the lowering placing that value in the trailing runtime argument, and the verifier rejecting widths that are illegal or narrower than `bits`.
BoarQing
force-pushed
the
fix/gbq-storage-type
branch
from
July 31, 2026 09:39
4b259d8 to
143c44e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GatherBlockQuantized's default zero point (used whenzero_pointsis absent) depends on the ONNX storage typeT1, which the EP currently cannot observe.tensor(uint4)andtensor(uint8)-holding-packed-nibbles both arrive at the runtime asui8withbits=4and a byte-identical layout, yet the ORT CPU kernel uses 0 for the former and 2^(bits-1) for the latter:Deciding from
is_signed_data, asmaindoes today, therefore biases one of the two by8 * scalewhichever way the condition is written. In practiceuint4embedding tables published withoutzero_pointscome back shifted by-8 * scaleon every gathered row.The correct rule is already written down in three places in this repo —
HipOps.td("the default is0for int4/uint4 and2^(bits-1)for uint8", from #368),hipdnn_ep_runtime.h, and the ORT schema doc string — but is not what the code implements.Approach
Carry the ONNX element type explicitly rather than trying to re-derive it:
mlir-graph.cpp) recordsonnx.element_typeon everyonnx.Constant. This attribute was already listed in the GBQ legalizer's preserve-set but nothing ever set it.convert-onnx-to-hippinsquant_storage_bitsfrom that marker, beforelegalizeInt4ConstantIfNeededrewrites the constant and erases the shape evidence that would otherwise distinguish the two cases.convert-hip-to-llvmforwards it to the runtime (22nd parameter).default_zp = (storage_bits == 8) ? 1 << (bits - 1) : 0.Signedness no longer participates in the zero-point decision at all. It is still needed for nibble sign extension, which is a separate concern — the previous code used one boolean for both jobs and got the second one wrong.
quant_storage_bitsis declared in ODS with a verifier rather than attached as a discardable attribute, so producer and consumer share a generated accessor and illegal widths are rejected at verification. It is optional and falls back tobits, keeping hand-written IR valid.Also routes the prepare-pattern attribute edits through
rewriter.modifyOpInPlaceinstead of mutating the op behind the driver's back.Verification
Built
mainand this branch separately and ran both against the same models with the same ORT CPU reference (hip-onnx-runner -L, EP vs-nCPU):zero_pointsmainL2uint4_nozpuint4_nozp_100352x2048int4_nozpuint8_nozpu8bits4_nozpuint4_zpuint4_zp_blk16int4_zpuint8_zpuint4_zp_100352x2048The failure signature on
mainis a pure additive shift, not noise or a layout error:u8bits4_nozpis the case that separates the two candidate rules: it is byte-identical touint4_nozpbut requires the opposite default. It passes on both builds, confirming the fix keys off storage type and not offbits.361/361 lit tests pass. ORCA-2bit runs end to end; it is unaffected either way because it ships
zero_points, so the default is never consulted.Affected models
Only when all three hold: storage is
tensor(uint4),bits == 4, andzero_pointsis absent. Anything shippingzero_pointsis unaffected.Not addressed here
Deliberately out of scope, flagged for follow-up:
bits == 2is legal fortensor(uint8)per the schema, butconvert-onnx-to-hipstill gates onbits in {4, 8}, so such models fall back rather than converting.quantize_axisis still re-derived from shape invariants when the attribute is absent, rather than using the ONNX default of 1.legalizeInt4ConstantIfNeededandrecreateExternalConstantstill returnfalse/nullptrsilently instead of emitting diagnostics.uint4_nozpandu8bits4_nozp) into the gate is the change that prevents a recurrence.Test plan