Skip to content

Vulkan: stop delegating nodes past their own support check - #21847

Open
msluszniak wants to merge 2 commits into
pytorch:mainfrom
msluszniak:ms/vulkan-index-tensor-support-check
Open

Vulkan: stop delegating nodes past their own support check#21847
msluszniak wants to merge 2 commits into
pytorch:mainfrom
msluszniak:ms/vulkan-index-tensor-support-check

Conversation

@msluszniak

@msluszniak msluszniak commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Vulkan backend fixes found while bringing up LFM2.5-VL (a hybrid
attention + gated short-conv VLM) on Android. Neither is specific to that model.

1. Nodes were delegated past their own support check.

Concretely: a rotary embedding's freqs_cos[input_pos] feeds the hf_rope
pattern, so its aten.index.Tensor was delegated even though
check_index_tensor_node rejects a non-1-D self. The gather then read
self[idx, 0, 0, 0] against a (4096, 64) table, and resize_index_tensor_node
set rank-1 sizes on a rank-2 tensor, aborting in virtual_resize with
"new sizes cannot modify the dimensionality of the tensor".

This PR excludes the placeholder bindings from the match.
The texture shader still assumes the 1-D form, so pick_io_storage_fn
routes a higher-rank self to a contiguous buffer.

2. VulkanBackend::execute underflows on a delegate arg-count mismatch.

output_offset = args.size() - num_outputs is size_t. If the serialized graph
declares more outputs than the delegate call has arguments, that subtraction
wraps and every output access reads through a wild pointer — the failure
surfaces as a SIGSEGV inside execute() with nothing pointing at the cause.

The reachable route today is a mutated buffer serialized as a graph output.
LFM2.5's ten short-conv states declared 11 outputs against a 3-argument call.

This PR checks the counts up front so the result is a readable error naming both
numbers rather than a segfault. It does not change the mutation handling itself.

Test plan

  • New test_vulkan_backend_index_tensor_higher_rank_self in
    backends/vulkan/test/test_vulkan_delegate.py, covering table[positions]
    with a 2-D table — the shape a rotary embedding uses. This fails before the
    first change and passes after.
  • Existing backends/vulkan/test/test_vulkan_delegate.py suite.
  • On device (Galaxy S26 Ultra, Adreno, driver 0842.19.8): LFM2.5-VL 450M and
    1.6B keep the rotary embedding inside the delegate, lower to a single Vulkan
    call for the decoder, and generate correct text and image responses. Before
    the first change the export aborted in virtual_resize; with it, decode runs
    at ~150 tok/s (450M) and ~72 tok/s (1.6B) versus 100 and 54 on XNNPACK, and
    text-only TTFT drops from 326→
    185 ms and 565→
    304 ms respectively.

cc @SS-JIA @manuelcandales @digantdesai @cbilgin

create_pattern_match_from_internal_match builds PatternMatch.all_nodes from
every entry in InternalMatch.nodes_map, which includes the target-graph nodes
bound to the PATTERN's placeholders. Those nodes merely feed the matched
subgraph; the fused op does not compute them, so the partitioner has no reason
to claim them.

That set becomes VulkanPartitioner's fusable_nodes, and _is_node_supported
returns True for anything in it before it consults the op's
are_node_inputs_supported_fn. Feeder nodes therefore have their support checks
silently waived, and an op can be delegated on shapes its implementation
rejects.

A rotary embedding hits this: `freqs_cos[input_pos]` feeds the hf_rope pattern,
so its aten.index.Tensor was delegated even though check_index_tensor_node
rejects a non-1-D `self`. The gather then read self[idx, 0, 0, 0] against a
(4096, 64) table, and resize_index_tensor_node set rank-1 sizes on a rank-2
tensor, aborting in virtual_resize with "new sizes cannot modify the
dimensionality of the tensor".

Exclude the placeholder bindings from the match. Since a gather along dim 0
never required a 1-D `self` in the first place, also generalize the op rather
than just declining it:

  * check_index_tensor_node now requires only that the INDEX is 1-D.
  * the buffer shader derives the gather axis from the output rank and passes
    self's trailing coordinates through unchanged. The texture variant still
    assumes the 1-D form, so pick_io_storage_fn routes a higher-rank `self` to
    a contiguous buffer.
  * resize_index_tensor_node computes index.sizes ++ self.sizes[1:] instead of
    reusing the index's sizes, which was only correct for a 1-D `self`.

Verified on a Galaxy S26 Ultra (Adreno): LFM2.5-VL keeps its rotary embedding
inside the delegate and generates correctly.
VulkanBackend::execute computes `output_offset = args.size() - num_outputs`
in size_t. If the serialized graph declares more outputs than the delegate
call has arguments, that subtraction wraps and every subsequent output access
reads through a wild pointer, so the failure surfaces as a SIGSEGV inside
execute() with nothing pointing at the real cause.

The reachable way to get there today is a mutated buffer serialized as a graph
output. alias_buffer_mutations avoids that by aliasing the mutation onto the
buffer's own value, but it is opt-in, and with it off a mutation that was
functionalized into a computed node -- rather than arriving as the buffer
placeholder, as sdpa_with_kv_cache's KV caches do -- still lands in output_ids.
A model with such a buffer then declares more outputs than the call supplies
and segfaults; LFM2.5's short-conv states declared 11 against a 3-argument call.

Check the counts up front so this is a readable error naming both numbers.
@msluszniak
msluszniak requested a review from SS-JIA as a code owner August 14, 2026 17:23
@pytorch-bot pytorch-bot Bot added the module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ label Aug 14, 2026
@pytorch-bot

pytorch-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⚠️ 13 Awaiting Approval

As of commit d2f0ad0 with merge base 33f68d1 (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 added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 14, 2026
@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants