Skip to content

[CPU] Fuse 16-bit QuantizeLinear -> DequantizeLinear into QuantizeDeq… - #33775

Draft
mirounga wants to merge 3 commits into
microsoft:mainfrom
mirounga:rs_cpu_a16_qdq
Draft

mirounga wants to merge 3 commits into
microsoft:mainfrom
mirounga:rs_cpu_a16_qdq

Conversation

@mirounga

Copy link
Copy Markdown
Contributor

…uantizeLinear

A16 models keep 16-bit activation Q/DQ boundaries around every quantized op, including the MatMulNBits nodes fused in #32885. On CPU each pair runs as two kernels and materializes an int16/uint16 tensor that is immediately dequantized again.

Add the com.microsoft.QuantizeDequantizeLinear op (float input, int16 or uint16 zero point) and a CPU kernel for it:

  • MlasQuantizeDequantizeLinear runs the per-element steps of the MLAS QuantizeLinear kernel and then (q - zp) * scale in one vector loop (SSE2, NEON, LSX). On other platforms, or when a non-default x64 quantize kernel is selected, it falls back to two passes through MlasQuantizeLinear.
  • Results are bitwise identical to the unfused QuantizeLinear -> DequantizeLinear pair.

Add QuantizeDequantizeLinearFusion, a Level3 CPU transformer that replaces a 16-bit QuantizeLinear whose consumers are all DequantizeLinear nodes with the same constant scalar scale and zero point by one fused node. It also follows the quantized tensor through data-movement ops (Transpose, Reshape, Slice, Gather, Split, Unsqueeze, ...) to matching DQs and runs those ops on the float values. It runs after the Level2 Q/DQ node-unit fusions, so it never removes a Q/DQ they could still use, and session.disable_quant_qdq disables it.

Tests:

  • QuantizeDequantizeLinearOpTest: int16 and uint16 round-half-to-even and saturation, a large tensor against a reference, empty input, and rejection of per-axis parameters.
  • MLAS QuantizeDequantizeLinearS16/U16: bitwise comparison with MlasQuantizeLinear followed by dequantization.
  • QuantizeDequantizeLinearFusionTests: interior pairs are bit-exact, fusion through data-movement ops, fan-out and graph-output DQs, A16 MatMulNBits boundaries, session.disable_quant_qdq, and the cases that must not fuse (8-bit, mismatched or non-constant or per-axis parameters, other uses of the quantized value, non-CPU EPs).

…uantizeLinear

A16 models keep 16-bit activation Q/DQ boundaries around every quantized
op, including the MatMulNBits nodes fused in microsoft#32885. On CPU each pair runs
as two kernels and materializes an int16/uint16 tensor that is immediately
dequantized again.

Add the com.microsoft.QuantizeDequantizeLinear op (float input, int16 or
uint16 zero point) and a CPU kernel for it:
- MlasQuantizeDequantizeLinear runs the per-element steps of the MLAS
  QuantizeLinear kernel and then (q - zp) * scale in one vector loop
  (SSE2, NEON, LSX). On other platforms, or when a non-default x64
  quantize kernel is selected, it falls back to two passes through
  MlasQuantizeLinear.
- Results are bitwise identical to the unfused QuantizeLinear ->
  DequantizeLinear pair.

Add QuantizeDequantizeLinearFusion, a Level3 CPU transformer that replaces
a 16-bit QuantizeLinear whose consumers are all DequantizeLinear nodes
with the same constant scalar scale and zero point by one fused node. It
also follows the quantized tensor through data-movement ops (Transpose,
Reshape, Slice, Gather, Split, Unsqueeze, ...) to matching DQs and runs
those ops on the float values. It runs after the Level2 Q/DQ node-unit
fusions, so it never removes a Q/DQ they could still use, and
session.disable_quant_qdq disables it.

Tests:
- QuantizeDequantizeLinearOpTest: int16 and uint16 round-half-to-even and
  saturation, a large tensor against a reference, empty input, and
  rejection of per-axis parameters.
- MLAS QuantizeDequantizeLinearS16/U16: bitwise comparison with
  MlasQuantizeLinear followed by dequantization.
- QuantizeDequantizeLinearFusionTests: interior pairs are bit-exact, fusion
  through data-movement ops, fan-out and graph-output DQs, A16
  MatMulNBits boundaries, session.disable_quant_qdq, and the cases that
  must not fuse (8-bit, mismatched or non-constant or per-axis
  parameters, other uses of the quantized value, non-CPU EPs).

Co-authored-by: Jovan Bubonja <jovanbubonja@microsoft.com>
@mirounga
mirounga requested review from Edward Chen (edgchen1) and Tianlei Wu (tianleiwu) and a balanced review from Copilot October 30, 2026 20:19
@mirounga
mirounga marked this pull request as draft October 30, 2026 20:20

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.

🟡 Changes recommended

The fusion accepts opset-23+ QuantizeLinear nodes with FLOAT16 precision but computes in FLOAT, changing model outputs.

1 open issue
What changed in this PR

This PR adds CPU fusion for 16-bit QuantizeLinear → DequantizeLinear pairs, reducing intermediate tensor storage and kernel overhead in A16 models, including the MatMulNBits boundaries preserved by #32885.

Changes:

  • Adds the com.microsoft.QuantizeDequantizeLinear operator, CPU kernels, and MLAS implementation.
  • Registers a Level3 fusion that follows compatible data-movement operations and respects session.disable_quant_qdq.
  • Adds numerical and graph-transformation tests, plus operator documentation.
File Description
onnxruntime/​test/​optimizer/​quantize_dequantize_linear_fusion_test.cc Tests fusion patterns, exclusions, and A16 boundaries.
onnxruntime/​test/​mlas/​unittest/​test_quantizelinear.cpp Adds bitwise comparisons against separate quantization and dequantization.
onnxruntime/​test/​contrib_ops/​quantize_dequantize_linear_op_test.cc Tests rounding, saturation, empty inputs, and parameter validation.
onnxruntime/​core/​optimizer/​qdq_transformer/​quantize_dequantize_linear_fusion.h Declares the fusion transformer.
onnxruntime/​core/​optimizer/​qdq_transformer/​quantize_dequantize_linear_fusion.cc Implements eligibility checks and graph rewriting.
onnxruntime/​core/​optimizer/​graph_transformer_utils.cc Registers the fusion at Level3.
onnxruntime/​core/​mlas/​lib/​quantize.cpp Implements vectorized fusion and two-pass fallback.
onnxruntime/​core/​mlas/​inc/​mlas.h Declares the fused MLAS API.
onnxruntime/​core/​graph/​contrib_ops/​quantization_defs.cc Defines the operator schema and shape inference.
onnxruntime/​core/​graph/​contrib_ops/​ms_opset.h Registers the schema in the Microsoft operator set.
onnxruntime/​contrib_ops/​cpu/​quantization/​quantize_dequantize_linear.cc Implements the threaded CPU operator.
onnxruntime/​contrib_ops/​cpu/​cpu_contrib_kernels.cc Registers int16 and uint16 CPU kernels.
docs/​OperatorKernels.md Lists supported CPU kernel types.
docs/​ContribOperators.md Documents the new operator.

Review effort: Balanced

…earFusion

From opset 23 the CPU QuantizeLinear kernel honors precision=FLOAT16, but the
fused kernel always computes in float, so fusing such a node changes the
outputs. Only fuse when precision is 0 or FLOAT, and add opset-23 tests for
both the rejected and the still-fused cases.

This branch has not been deployed

No deployments
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.

2 participants