Repository navigation
[ET-VK] Fuse torchao int8 weight-only linear into linear_q8csw - #24082
msluszniak wants to merge 2 commits into
Conversation
A torchao int8 per-channel weight (dequantize_affine with [N, 1] scales) only matched the 4-bit routes, so the dequantize_affine stayed in the delegate, which has no kernel for it, and the program aborted at load. Route it to the existing linear_q8csw kernel and give that op its AOT definition.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24082
Note: Links to docs will display an error until the docs builds have been completed. ⏳ No Failures, 1 PendingAs of commit 8df4f7a with merge base 7645c90 ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
SS-JIA
left a comment
There was a problem hiding this comment.
Requesting changes based on an independent, AI-assisted static review (Claude) of the current head, which I've gone through and agree with. Each finding is posted as an inline comment with a suggested fix; where a finding is about code outside this PR's diff, the comment sits on the closest related line and its Where line gives the actual location.
Summary
- What it does: adds an AOT definition for the existing runtime op
et_vk.linear_q8cswand a newreplace_quantized_linear_patternsbranch. torchao int8 per-channel weight-only linears (dequantize_affinewith[N, 1]scales) now fuse into that op instead of leavingtorchao.dequantize_affine, which has no kernel, in the delegate. That case works: it delegates as a singleet_vk.linear_q8cswfor fp32, fp16, and dynamic shapes, and the pass test passes. - Main problem: the branch does not check the dequant op or the bit width. It also matches
quantized_decomposed.dequantize_per_channel([N]scales) weight-only linears. Previously these fell through toFuseQuantizedOpsTransform, which runs afterFusePatternsPass. As a result:- PT2E 8-bit weight-only now lowers to
linear_q8csw, notaten._weight_int8pack_mmplusadd. The runtime implementation is the same, so this is benign but unannounced. - PT2E 4-bit weight-only now lowers to
linear_q8csw, notlinear_qcs4w. This is a regression (A1).
- PT2E 8-bit weight-only now lowers to
- Merges into current main with no conflicts. Merged #24055 and #24116 are already in the PR base, and no semantic overlap was found with them.
- Semantic conflict with open #24971. That PR gates the detector on
has_fused_op(), which returns False for torchao int8 per-channel, and it asserts that this case stays undelegated. Whichever lands second must reconcile both the code and the tests (A2). This PR is right for its case: after #24971 alone, a torchao int8 weight-only linear fails at export with "Missing out variants". #24971's gating is right for the cases that still have no route. - Still broken (acknowledged in the PR summary, not a regression): int8 per-group, 8da8w, K % 4 != 0, and non-zero weight zero points all still delegate
torchao.dequantize_affinewithout a kernel. I verified the int8PerGroup(32)and K=66 cases. - Open #22776 conflicts textually in the same region (method added after
is_weight_perchannel_quantized). It is already stale against main, and there is no semantic interaction.
Findings not tied to a line
A6 (Nit): PR description understates scope
- Where: PR summary
- Issue: The summary describes only the torchao case. PT2E per-channel 8-bit weight-only linears also move from
aten._weight_int8pack_mmplus a separateaten.addtolinear_q8cswwith fused bias. That leaves the 8-bit branch ofFuseQuantizedOpsTransformreachable only for linears the pattern rejects (e.g. K % 4 != 0). - Suggested fix: Mention the PT2E routing change in the summary and test plan.
| elif ( | ||
| match.is_weight_only_quantized() | ||
| and match.is_weight_per_output_channel_quantized() | ||
| and weight_tensor.dtype == torch.int8 |
There was a problem hiding this comment.
A1 (High): PT2E 4-bit per-channel weight-only linear rerouted from linear_qcs4w to linear_q8csw; breaks WebGPU
-
Where:
backends/vulkan/patterns/quantized_linear.py:725-731 -
Issue: The new branch accepts any weight-only, per-output-channel linear with an int8-typed weight and zero zero points. PT2E 4-bit per-channel quantization (
VulkanQuantizerwithget_symmetric_quantization_config(is_dynamic=False, weight_bits=4)) stores 4-bit values in an int8 tensor and uses[N]scales. Theq4gswbranch rejects it becauseis_weight_pergroup_quantizedrequires 2-D scales, so it reaches the new branch.FusePatternsPassruns beforeFuseQuantizedOpsTransforminvulkan_preprocess.py, so the linear is fused intolinear_q8cswbefore the existinglinear_qcs4wroute can see it.- Vulkan: weights are no longer 4-bit packed, so weight memory doubles and the op uses a different kernel.
- WebGPU: it reuses the Vulkan patterns and does not skip
quantized_linear. The WebGPU runtime implementslinear_qcs4wbut notlinear_q8csw, so these models fail at delegate init.
-
Evidence: Executed. Ad-hoc lowering through
VulkanPartitioner,nn.Linear(64, 128), delegate ops:- origin/main:
['et_vk.linear_qcs4w.default', 'et_vk.prepack.default', 'aten.add.Tensor'] - PR head:
['et_vk.linear_q8csw.default']
The existing WebGPU test
backends/webgpu/test/ops/test_linear_qcs4w.py::LinearQcs4wTest::test_delegatesfails on the PR head:AssertionError: b'linear_qcs4w' not found in .... The serialized buffer containset_vk.linear_q8csw.default. This test is not in the PR's test plan. - origin/main:
-
Suggested fix: Exclude 4-bit-range weights so they keep the
FuseQuantizedOpsTransform/linear_qcs4wroute. torchao[N, 1]4-bit weights are already taken by theq4gswbranch above.and weight_tensor.dtype == torch.int8 and not utils.is_in_4bit_range(weight_tensor) and bool(torch.all(weight_zeros_tensor == 0))
Verified: with this line, the PT2E 4-bit case lowers to
linear_qcs4wagain, the torchao int8 cases still fuse tolinear_q8csw, andtest_linear_qcs4w.pyplus the-k "qcs4w or delegates or linear"subset oftest_vulkan_passes.pypass (9 passed). An alternative is to restrict the branch tomatch.dequantize_weight_node.target == exir_ops.edge.torchao.dequantize_affine.default, which also leaves PT2E 8-bit on_weight_int8pack_mm.
| ) | ||
| self.assertTrue(torch.allclose(emb_x + emb_y, eager_ref, atol=1e-3, rtol=1e-3)) | ||
|
|
||
| def test_fuse_torchao_int8_weight_only_linear(self): |
There was a problem hiding this comment.
A2 (Medium): Semantic conflict with open #24971 ("Do not delegate quantized linears that have no Vulkan kernel")
-
Where:
backends/vulkan/patterns/quantized_linear.py(detectorfind_quantized_linear_patternsvs. replacement routing);backends/vulkan/test/test_vulkan_passes.py:1338 -
Issue: Do not delegate quantized linears that have no Vulkan kernel (#24971) #24971 adds
has_fused_op()to the detector. For a torchao weight-only linear that is not 4-bit per-group, it returns False:is_weight_4bit_quantized()is False for the int8 range, anddequantize_affineis not adequantize_per_channelnode. Do not delegate quantized linears that have no Vulkan kernel (#24971) #24971 also assertsmatches(w8, 8) == [False]and thatdequantize_affinestays outside the delegate. If both land as written:- this PR's branch becomes dead code and its test fails (
result.modified/linear_q8cswcount), or - Do not delegate quantized linears that have no Vulkan kernel (#24971) #24971's tests fail, depending on how the conflict is resolved.
Both PRs also edit the same lines, which caused textual conflicts in
quantized_linear.pyandtest_vulkan_passes.pywhen I applied Do not delegate quantized linears that have no Vulkan kernel (#24971) #24971's Vulkan hunks onto this head withgit apply -3. - this PR's branch becomes dead code and its test fails (
-
Evidence: Static reasoning from
gh pr diff 24971. The textual conflicts were observed withgit apply --check -3. -
Suggested fix: Keep both behaviors. Do not delegate quantized linears that have no Vulkan kernel (#24971) #24971's
has_fused_op()should return True for torchao weight-only per-output-channel 8-bit, and itsw8assertions should flip to delegatedlinear_q8csw. Better still, have the detector and the replacement share one routing predicate so they cannot diverge again. That divergence is the root cause of the original bug: the partitioner accepts every node of a match unconditionally (vulkan_partitioner.py:299). Coordinate the landing order with Do not delegate quantized linears that have no Vulkan kernel (#24971) #24971's author.
| fused_out = torch.ops.et_vk.linear_q8csw.default( | ||
| sample_inputs[0], weight, scales, bias | ||
| ) | ||
| self.assertTrue(torch.allclose(fused_out, eager_ref, atol=1e-5, rtol=1e-5)) |
There was a problem hiding this comment.
A3 (Medium): Tests miss the routing side effects and the end-to-end lowering
- Where:
backends/vulkan/test/test_vulkan_passes.py:1338-1393 - Issue: The new test runs only
FusePatternsPasson one torchao config. It does not cover:- PT2E per-channel 8-bit and 4-bit, whose routing this PR changes (A1 would have been caught);
- partitioner-level lowering (no remaining
dequantize_affinein the delegate, delegate op list); - the no-bias /
mmanchor variant.
- Evidence: A1 passes the PR's test plan but fails an existing WebGPU test.
- Suggested fix: Add a
to_edge_transform_and_lower(..., partitioner=[VulkanPartitioner()])test asserting that the delegate containslinear_q8cswand nodequantize_affine. Add a PT2Eweight_bits=4per-channel case assertinglinear_qcs4w(or rely on the WebGPU test in CI). Add abias=Falsecase.
| # scales should have same size as weight's output channels dim | ||
| return scales_shape[0] == weight_shape[-2] | ||
|
|
||
| def is_weight_per_output_channel_quantized(self) -> bool: |
There was a problem hiding this comment.
A4 (Low): Near-duplicate predicate and replacement body
- Where:
backends/vulkan/patterns/quantized_linear.py:270-276,368-405 - Issue:
is_weight_per_output_channel_quantizedduplicatesis_weight_perchannel_quantizedapart from accepting[N, 1].make_linear_q8csw_oprepeats the bias-padding and create/replace block frommake_linear_q4gsw_opalmost line for line. Extendingis_weight_perchannel_quantizeddirectly is unsafe, because theq8ta_q8cswroute would then pad[N, 1]scales along the wrong dimension, so a separate check is justified. - Suggested fix: Give the existing predicate an
allow_keepdim: bool = Falseparameter instead of adding a second method. Optionally factor the shared "pad bias, insert node beforeoutput_node, copymeta['val'], replace uses" logic into a helper used by the weight-only makers.
| ################# | ||
|
|
||
|
|
||
| def linear_q8csw( |
There was a problem hiding this comment.
A5 (Low): WebGPU still has no kernel for the newly emitted op
- Where:
backends/webgpu/partitioner/partitioner.py:83(_WEBGPU_SKIP_FUSION_PATTERNS),backends/webgpu/runtime/ops/ - Issue: With this PR, WebGPU lowers torchao int8 per-channel weight-only linears to
et_vk.linear_q8csw. The WebGPU runtime does not implement that op; it registers onlyet_vk.linear_q4gswamong the weight-only linears. This is not a regression: before the PR, the same models delegateddequantize_affine, which WebGPU also lacks. The failure mode stays "delegate init failure" instead of falling back to CPU. - Suggested fix: Out of scope for this PR. Note it for the WebGPU owners, or fold it into the shared-predicate work in A2 with a backend-specific route filter.
Summary
A torchao int8 per-channel weight-only linear (
IntxWeightOnlyConfig(int8, PerAxis(0))) matched no route inquantized_linear.py, so itstorchao.dequantize_affinewas serialized into the delegate, which has no kernel for it, and the model aborted at load. Route it to the existinglinear_q8cswkernel, which had no AOT definition. Per-group int8 and 8da8w still have no route.Test plan
New
test_fuse_torchao_int8_weight_only_linearfails before, passes after;test_vulkan_passes.pypasses.cc @SS-JIA @manuelcandales @digantdesai @cbilgin @nil-is-all