Skip to content

Pass the floating point activation to et_vk.linear_q8ta_q8csw - #22776

Open
giuliocorradi wants to merge 1 commit into
pytorch:mainfrom
giuliocorradi:vulkan-q8ta-fp-input
Open

Pass the floating point activation to et_vk.linear_q8ta_q8csw#22776
giuliocorradi wants to merge 1 commit into
pytorch:mainfrom
giuliocorradi:vulkan-q8ta-fp-input

Conversation

@giuliocorradi

Copy link
Copy Markdown

Fixes #22775

Summary

The Vulkan implementation of linear_q8ta_q8csw quantizes the activation tensor itself. QuantizedLinear.cpp names the argument fp_input, allocates a temporary packed int8 tensor for it, and hands it to add_quantize_and_pack_4h4w_node:

    const ValueRef fp_input,
    ...
    TmpTensor packed_int_input(
        &graph, graph.sizes_of(fp_input), vkapi::kInt8x4,
        utils::kBuffer, utils::kPackedInt8_4H4W);

    if (!input_quant_config.is_dynamic) {
      add_quantize_and_pack_4h4w_node(
          graph, input_quant_config, fp_input, ...);

The AOT fusion was passing the quantized tensor instead. In QuantizedLinearMatch.__init__, pattern_input_node is set to the int8 output of the input quantize node, and only the dynamic-quantization branch steps back past it to the float:

    self.pattern_input_node = input_to_dq_node          # the int8 quantize output
    ...
    if utils.is_quant_node(input_to_dq_node) and utils.is_dynamic_qscale(
        self.input_scales_node
    ):
        self.quantize_input_node = input_to_dq_node
        self.pattern_input_node = self.quantize_input_node.args[0]   # the float

So a statically quantized activation reached the op as int8. The program lowered and serialized fine, then failed at the first inference asking for a shader variant whose name carries an input dtype that cannot exist:

get_shader_info at backends/vulkan/runtime/api/ShaderRegistry.cpp:51:
  (it != listings_.end()) is false!
Could not find ShaderInfo with name clone_buffer_to_image_int8_int32

The change

Add QuantizedLinearMatch.get_fp_input_node() to resolve the floating point activation, and use it when building the op.

Where the activation is genuinely only available quantized, there is nothing to step back to, and the op still needs a float — this happens when the int8 tensor is a graph input, and when it is produced by a preceding linear that pattern replacement already turned into q8ta_linear. In that case a dequantize_per_tensor node is inserted so the op receives what it expects. That case aborts today, so the extra dequantize is strictly an improvement; a follow-up could avoid the round trip with an int8-input variant of the op.

q8ta_linear is deliberately untouched: it takes packed_int8_input and keeps receiving pattern_input_node.

Why this was not caught

No bundled quantizer produces this op. XNNPACKQuantizer also quantizes the linear's output, which routes to q8ta_linear, and VulkanQuantizer.get_symmetric_quantization_config has only weight-only (is_dynamic=Falseact_quantization_spec = None) and dynamic modes — no static per-tensor activation mode, which is the one thing is_input_static_per_tensor_quantized() matches on. The added test therefore builds the quantize/dequantize pattern directly.

Test plan

python -m unittest backends.vulkan.test.test_vulkan_passes -v

12 tests pass. The new test, test_linear_q8ta_q8csw_takes_floating_point_input, fails without the source change:

AssertionError: True is not false : linear_q8ta_q8csw was given the
quantized activation; it expects the floating point one

Also verified end to end on an AMD Radeon 8060S (RADV GFX1151, RDNA 3.5), running the lowered .pte through _load_for_executorch and comparing against the eager module:

case before after
single static-quant linear shader error max|d| 2.3e-05
2 chained quantized linears shader error max|d| 1.2e-04
3 chained quantized linears shader error max|d| 1.1e-04
int8 activation as a graph input shader error max|d| 1.8e-05

backends.vulkan.test.test_vulkan_delegate produces an identical set of results before and after (the failures in that suite in my environment are pre-existing and unrelated).

Why it matters

On RDNA 3.5 this op is the path to v_wmma_i32_16x16x16_iu8, the INT8 matrix instruction. On this part llama.cpp's Vulkan backend reaches 13.71 TFLOPS with int8 weights on the same shape where this backend reaches 6.5 in fp32, while the fp32 paths are close (6.50 vs 7.40) — so the quantized path is where the remaining performance is.

Found while lowering openpi's π₀.₅ to the Vulkan delegate.

The Vulkan implementation of linear_q8ta_q8csw quantizes the activation
tensor itself: QuantizedLinear.cpp names the argument fp_input, allocates
a temporary packed int8 tensor for it, and passes it to
add_quantize_and_pack_4h4w_node.

The AOT fusion was passing the quantized tensor instead. In
QuantizedLinearMatch, pattern_input_node is set to the int8 output of the
input quantize node, and only the dynamic quantization branch steps back
past it to the floating point tensor. Statically quantized inputs
therefore reached the op as int8, and the delegate failed at the first
inference looking for a shader variant that takes an already quantized
input:

  Could not find ShaderInfo with name clone_buffer_to_image_int8_int32

Add QuantizedLinearMatch.get_fp_input_node() to resolve the floating
point activation, and use it when building the op. Where the activation
is only available as a quantized tensor - it is a graph input, or it is
produced by a preceding op that was already replaced with q8ta_linear -
insert a dequantize node so the op still receives what it expects.

q8ta_linear is unaffected: it takes packed_int8_input and continues to
receive pattern_input_node.

Note that no bundled quantizer produces this op today, which is why the
mismatch went unnoticed. XNNPACKQuantizer also quantizes the linear's
output, which selects q8ta_linear, and VulkanQuantizer has no static
activation quantization mode. The added test therefore builds the
quantize/dequantize pattern directly.

Test Plan:

  python -m unittest backends.vulkan.test.test_vulkan_passes -v

12 tests pass. The new test,
test_linear_q8ta_q8csw_takes_floating_point_input, fails without this
change with "linear_q8ta_q8csw was given the quantized activation; it
expects the floating point one".

Also verified end to end on an AMD Radeon 8060S (RADV GFX1151), running
the lowered .pte and comparing against the eager module:

  case                                    before          after
  single static-quant linear              shader error    max|d| 2.3e-05
  2 chained quantized linears             shader error    max|d| 1.2e-04
  3 chained quantized linears             shader error    max|d| 1.1e-04
  int8 activation as a graph input        shader error    max|d| 1.8e-05

backends.vulkan.test.test_vulkan_delegate shows an identical set of
results before and after the change.
Copilot AI lite review requested due to automatic review settings September 13, 2026 03:03
@pytorch-bot

pytorch-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22776

Note: Links to docs will display an error until the docs builds have been completed.

⚠️ 16 Awaiting Approval

As of commit 793fad1 with merge base 903696a (image):

AWAITING APPROVAL - The following workflows need approval before CI can run:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla

meta-cla Bot commented Sep 13, 2026

Copy link
Copy Markdown

Hi @giuliocorradi!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@linux-foundation-easycla

Copy link
Copy Markdown

CLA Not Signed

@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

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 new fallback path in make_linear_q8ta_q8csw_custom_op hard-codes a dequantize op (qmin/qmax/dtype) instead of reusing the existing dequantize node, which can diverge from the graph’s original dequantization parameters.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR fixes Vulkan AOT fusion for et_vk.linear_q8ta_q8csw so the custom op receives the floating-point activation (which the runtime quantizes internally) instead of incorrectly receiving the already-quantized int8 tensor in the statically-quantized activation case.

Changes:

  • Add QuantizedLinearMatch.get_fp_input_node() to resolve the correct floating-point activation input for linear_q8ta_q8csw.
  • Update make_linear_q8ta_q8csw_custom_op to use the resolved fp activation when constructing the op.
  • Add a unit test that asserts linear_q8ta_q8csw is not fed by an activation quantize node (and is not int8-typed).
File summaries
File Description
backends/vulkan/patterns/quantized_linear.py Resolve and pass the floating-point activation to et_vk.linear_q8ta_q8csw during pattern replacement.
backends/vulkan/test/test_vulkan_passes.py Add regression test ensuring linear_q8ta_q8csw takes a floating-point activation input.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +527 to +548
# This op quantizes the activation tensor itself, so it takes the floating
# point input rather than the quantized one.
fp_input_node = match.get_fp_input_node()
if fp_input_node is None:
# The input is only available as a quantized tensor, i.e. it is produced
# by a preceding quantized op. Dequantize it so that the op has the
# floating point tensor it expects.
with graph_module.graph.inserting_before(match.output_node):
fp_input_node = graph_module.graph.create_node(
"call_function",
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
args=(
match.pattern_input_node,
match.input_scales_node,
match.input_zeros_node,
-128,
127,
torch.int8,
),
)
fp_input_node.meta["val"] = match.dequantize_input_node.meta["val"]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 793fad1bf4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +253 to +254
if utils.is_quant_node(self.pattern_input_node):
return self.pattern_input_node.args[0] # pyre-ignore[7]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unsupported activation quantization before bypassing it

When the matched static quantizer uses a valid non-signed-int8 configuration, such as torch.uint8 with range [0, 255], this unconditionally bypasses the quantize node without inspecting its dtype or bounds. The fused op then requantizes the float input using quantize_and_pack in linear_int8_input_block.glslh, which hard-codes [-128, 127]; the fallback below likewise forces those bounds and torch.int8. Because is_input_static_per_tensor_quantized() checks only that the scale is a float, these patterns are currently accepted and can produce severely incorrect results rather than preserving the original quantize/dequantize semantics. Validate that the matched q/dq pair uses the supported [-128, 127] int8 scheme, or skip the fusion.

Useful? React with 👍 / 👎.

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.

[ET-VK] Static activation quantization passes the int8 tensor to linear_q8ta_q8csw, which expects the float

3 participants