Skip to content

[QNN EP] Add 2-bit BQ STD Symmetric Support - #787

Open
qti-ashwshan wants to merge 4 commits into
mainfrom
dev/ashwshan/w2a16-bw-block-mapped
Open

qti-ashwshan wants to merge 4 commits into
mainfrom
dev/ashwshan/w2a16-bw-block-mapped

Conversation

@qti-ashwshan

Copy link
Copy Markdown
Collaborator

Description

  • Add 2-bit BQ Standard Symmetric support via BW_BLOCK_MAPPED encoding
  • Skip DQ insertion and use native output dtype for BW_BLOCK_MAPPED (ProcessAttributesAndOutputs)

Motivation and Context

@qti-ashwshan qti-ashwshan self-assigned this Aug 31, 2026
…encoding

Use QNN_QUANTIZATION_ENCODING_BW_BLOCK_MAPPED with STANDARD_SYMMETRIC
mapping for 2-bit weights with symmetric zero-points and 16-bit quantized
activations. This keeps activations in int16 natively without the int16->fp16
Dequantize workaround needed by the BW_FLOAT_BLOCK path.

- Add BwBlockMapped() factory and deep-copy support in QnnQuantParamsWrapper
- Add IsBwBlockMapped() predicate and include in IsBlockQuantized()
- Wire BW_BLOCK_MAPPED path in MatMulNBits op builder (ProcessInputs)
- Skip DQ insertion and use native output dtype for BW_BLOCK_MAPPED (ProcessAttributesAndOutputs)
- Add unit tests for W2A16 std symmetric with/without explicit ZP tensor
@qti-ashwshan
qti-ashwshan force-pushed the dev/ashwshan/w2a16-bw-block-mapped branch from dbbab47 to e8e4368 Compare September 12, 2026 04:36

@1duo 1duo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments inline. #1 (matmulnbits_op_builder.cc, asymmetric W2A16 path) is blocking: it breaks the build on QAIRT ≥ 2.51 after the merge from main.

Also: BwBlockMapped() and the BW_BLOCK_MAPPED deep copy in Init (copy constructor and operator=) have no host-side tests in qnn_quant_params_wrapper_test.cc. The only coverage is the HTP integration tests, which need aarch64/Linux with V69+. A small host test should check that copies own their own blockSize/scaleOffset buffers, and that bitwidth and mapping survive the copy.

RETURN_IF_NOT(zp_tensor_proto != nullptr, "MatMulNBits zero_points must be a constant initializer.");
RETURN_IF_ERROR(qnn_model_wrapper.UnpackInitializerData(zp_tensor_proto, per_block_uint8_zp));
std::vector<float> per_block_float_zp;
UnpackDataToDatatype<float>(per_block_uint8_zp, bits, num_zp_per_uint8, per_block_float_zp);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line won't compile after the merge from main. #856 removed UnpackDataToDatatype<T> and num_zp_per_uint8, and neither name exists in the tree anymore. CI builds with QAIRT 2.50.40 (qcom/packages.yml), which removes this block before compiling, so CI stays green anyway.

Restoring the helper wouldn't be correct either. The flat unpack ignores the per-row zero-point padding that #856 added. For bits=2, K=160, block_size=32: k_blocks=5, so each row's zero points take 2 bytes. That gives 8 unpacked values per row against 5 scales, and trips the scales.size() == offsets.size() assert in BwBlockMapped.

per_block_float_zp (from step 2.3 above) already holds these values, with padding handled and the shift and negate applied. Suggest dropping the re-unpack:

} else {
  mapping = QNN_QUANTIZATION_ENCODING_MAPPING_ASYMMETRIC_PLUS_ONE;
  per_block_int32_offset.assign(per_block_float_zp.begin(), per_block_float_zp.end());
}


// W2A16 Standard Symmetric BW_BLOCK_MAPPED (bits=2, 16-bit activation, symmetric zero-point).
// On SDK >= 2.51 uses BW_BLOCK_MAPPED encoding; on older SDKs falls back to BW_FLOAT_BLOCK.
TEST_F(QnnHTPBackendTests, MatMulNBits_BwBlockMapped_W2A16_StdSym_M1_N4_K64_BS16) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These four tests pass std::nullopt for expect_native_bq, so the QNN graph JSON is never dumped or checked. They pass with any encoding, including a silent fallback to BW_FLOAT_BLOCK. On the current CI SDK (2.50) the new code path doesn't run at all. Could you check the graph as the native-BQ tests do? For example, expect only the Quantize and Dequantize nodes from the QDQ model, with no extra INT16→FP16 Dequantize.

// (BLOCK) both produce the actual output data type (e.g. uint16/int16 for QDQ models) directly.
// Only BW_FLOAT_BLOCK forces the kernel to compute in FP16; LPBQ (BLOCKWISE_EXPANSION), native BQ
// (BLOCK), and BW_BLOCK_MAPPED all produce the actual output data type (e.g. uint16/int16) directly.
// NOTE: IsBlockQuantized() is true for both BLOCK and BW_FLOAT_BLOCK, so match the encoding directly.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this NOTE is out of date. IsBlockQuantized() now also covers BW_BLOCK_MAPPED. The same comment appears in conv_op_builder.cc (in ProcessAttributesAndOutputs).

params_.quantizationEncoding == QNN_QUANTIZATION_ENCODING_BW_BLOCK_MAPPED);
}

bool IsBwBlockMapped() const {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing calls IsBwBlockMapped(). Either use it (for example, in a test that checks the chosen encoding) or remove it.


// W2A16 Asymmetric BW_BLOCK_MAPPED (bits=2, 16-bit activation, asymmetric zero-point).
// Uses ASYMMETRIC_PLUS_ONE mapping: {0,1,2,3} → {-1,0,1,2}.
TEST_F(QnnHTPBackendTests, MatMulNBits_BwBlockMapped_W2A16_AsymPlusOne_M1_N4_K64_BS16) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this test sets has_zero_point = true but doesn't end in _ZP, unlike the other zero-point tests in this file (StdSym_..._BS32_ZP above follows the convention).

RunHtpQDQMatMulNBitsTest<2, int16_t>(params, std::nullopt, ExpectedEPNodeAssignment::All, QDQTolerance(0.02f));
}

TEST_F(QnnHTPBackendTests, MatMulNBits_BwBlockMapped_W2A16_AsymPlusOne_M1_N8_K128_BS32) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this test sets has_zero_point = true but doesn't end in _ZP, unlike the other zero-point tests in this file (StdSym_..._BS32_ZP above follows the convention).

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