Skip to content

[QNN] 16a8w: gathered tensor read wrong by a consumer that only feeds a graph output #24125

Open

Description

@msluszniak

🐛 Describe the bug

In a 16a8w HTP graph, a tensor gathered in-graph (table[pos]) is read wrong by its last consumer when that consumer's result only feeds a graph output. Below, cos/sin are gathered once and used by two RoPEs: output 0 (which also feeds a bmm) is correct, output 2 is not (rel err 0.77 vs 0.0017 for the QDQ module on host). Position p gets row p % 40 for most rows.

Not affected: the same graph in fp16, passing cos/sin as inputs instead of gathering them, or a single RoPE. We hit it in a TTS model where the last layer's keys are a method output; returning one more tensor computed from them works around it.

import numpy as np
import torch
from executorch.backends.qualcomm.export_utils import make_quantizer
from executorch.backends.qualcomm.quantizer.quantizer import QuantDtype
from executorch.backends.qualcomm.serialization.qc_schema import QcomChipset
from executorch.backends.qualcomm.utils.utils import (
    generate_htp_compiler_spec, generate_qnn_executorch_compiler_spec, to_edge_transform_and_lower_to_qnn)
from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e

H, N, D = 6, 320, 128


def rope(x, cos, sin):
    return x * cos + torch.cat([-x[..., D // 2:], x[..., :D // 2]], -1) * sin


class M(torch.nn.Module):
    def __init__(self):
        super().__init__()
        ang = torch.arange(2048).float()[:, None] / 1e6 ** (torch.arange(0, D, 2).float() / D)
        self.register_buffer("cos_t", torch.cat([ang.cos(), ang.cos()], -1))
        self.register_buffer("sin_t", torch.cat([ang.sin(), ang.sin()], -1))

    def forward(self, x0, x1, pos):
        cos, sin = self.cos_t[pos.long()], self.sin_t[pos.long()]
        k0 = rope(x0, cos, sin)
        # k0 also feeds a bmm; k1 feeds only the graph output and comes out wrong
        return k0, torch.bmm(k0, x0.transpose(1, 2)), rope(x1, cos, sin)


torch.manual_seed(0)
sample = lambda: (torch.randn(H, N, D) * 3, torch.randn(H, N, D) * 3, torch.arange(N, dtype=torch.int32))  # noqa: E731
m, ex = M().eval(), sample()
q = prepare_pt2e(torch.export.export(m, ex).module(), make_quantizer(quant_dtype=QuantDtype.use_16a8w, soc_model="SM8850"))
for _ in range(8):
    q(*sample())
q = convert_pt2e(q)
specs = generate_qnn_executorch_compiler_spec(soc_model=QcomChipset.SM8850,
                                              backend_options=generate_htp_compiler_spec(use_fp16=False))
with open("rope_gather.pte", "wb") as f:
    f.write(to_edge_transform_and_lower_to_qnn(q, ex, specs).to_executorch().buffer)
for i, t in enumerate(ex):
    t.numpy().tofile(f"input_{i}.raw")
np.save("expected_out2.npy", m(*ex)[2].numpy())
print("QDQ rel err of output 2:", float((q(*ex)[2] - m(*ex)[2]).norm() / m(*ex)[2].norm()))

Run rope_gather.pte with the three inputs and compare output 2 with expected_out2.npy.

Versions

ExecuTorch 1.4.1 (our fork of v1.4.1, not yet checked on main), QAIRT 2.47.0.260601, SM8850 (Galaxy S26 Ultra, HTP v81).

cc @cccclai @winskuo-quic @shewu-quic @haowhsu-quic @DannyYuyang-quic @qti-horodnic @cbilgin @abhinaykukkadapu @psiddh

Activity

  1. added theissue type on Oct 29, 2026
  2. added
    partner: qualcommFor backend delegation, kernels, demo, etc. from the 3rd-party partner, Qualcomm
    module: qnnIssues related to Qualcomm's QNN delegate and code under backends/qualcomm/
    on Oct 29, 2026
  3. yenhao-huang commented on Nov 6, 2026

    @yenhao-huang
    Contributor

    It seems the issue also happen beyond RoPE: in the 16a8w quantization setting, a simple index + element-wise mul also gives wrong results on HTP, possibly due to an op-fusion issue.

    import subprocess, sys
    import numpy as np, torch
    from executorch.backends.qualcomm.export_utils import make_quantizer
    from executorch.backends.qualcomm.quantizer.quantizer import QuantDtype
    from executorch.backends.qualcomm.serialization.qc_schema import QcomChipset
    from executorch.backends.qualcomm.utils.utils import (
        generate_htp_compiler_spec, generate_qnn_executorch_compiler_spec, to_edge_transform_and_lower_to_qnn)
    from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e
    
    
    class M(torch.nn.Module):
        def __init__(self):
            super().__init__()
            self.register_buffer("table", torch.randn(192, 32))
    
        def forward(self, x, pos):
            return x * self.table[pos.long()]  # aten.index.Tensor -> QNN Gather, then Mul
    
    
    torch.manual_seed(0)
    m, ex = M().eval(), (torch.randn(192, 32), torch.arange(192, dtype=torch.int32))
    q = prepare_pt2e(torch.export.export(m, ex).module(), make_quantizer(quant_dtype=QuantDtype.use_16a8w))
    q(*ex)
    q = convert_pt2e(q)
    specs = generate_qnn_executorch_compiler_spec(QcomChipset.SM8850, generate_htp_compiler_spec(use_fp16=False))
    open("m.pte", "wb").write(to_edge_transform_and_lower_to_qnn(q, ex, specs).to_executorch().buffer)
    for i, t in enumerate(ex):
        t.numpy().tofile(f"in{i}.raw")
    open("inputs.txt", "w").write("in0.raw in1.raw")
    subprocess.run([sys.argv[1], "--model_path=m.pte", "--input_list_path=inputs.txt", "--output_folder_path=."], check=True)
    
    ref = m(*ex)
    out = torch.from_numpy(np.fromfile("output_0_0.raw", np.float32)).view_as(ref)
    rel = lambda y: ((y - ref).norm() / ref.norm()).item()
    print(f"relative L2 vs fp32 -- QDQ: {rel(q(*ex)):.4f}, QNN HTP: {rel(out):.4f}")
  4. added
    triagedThis issue has been looked at a team member, and triaged and prioritized into an appropriate module
    on Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugmodule: qnnIssues related to Qualcomm's QNN delegate and code under backends/qualcomm/partner: qualcommFor backend delegation, kernels, demo, etc. from the 3rd-party partner, QualcommtriagedThis issue has been looked at a team member, and triaged and prioritized into an appropriate module

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions