Skip to content

[AMDGPU] Fix cross-half shuffle select for top-half lanes on wave64#769

Open
ZJLi2013 wants to merge 1 commit into
Genesis-Embodied-AI:mainfrom
ZJLi2013:amd-crosshalf-shuffle-upperhalf-fix
Open

[AMDGPU] Fix cross-half shuffle select for top-half lanes on wave64#769
ZJLi2013 wants to merge 1 commit into
Genesis-Embodied-AI:mainfrom
ZJLi2013:amd-crosshalf-shuffle-upperhalf-fix

Conversation

@ZJLi2013

@ZJLi2013 ZJLi2013 commented Jul 9, 2026

Copy link
Copy Markdown

Summary

On wave64 AMD GPUs, every cross-half subgroup shuffle (subgroup.shuffle, shuffle_up, shuffle_down) returns the wrong value on the upper half of the wave (lanes 32..63). amdgpu_cross_half_shuffle_i32 picks between the two ds_bpermute reads with a self-relative condition, but the reads are absolute, so the select is inverted for the top-half lanes.

The most visible consequence is that subgroup.inclusive_*_tiled / exclusive_*_tiled (Hillis-Steele scans, built on shuffle_up) miscompile from lane 32 onward on a full wave64. reduce_*_tiled / shuffle_down happen to only consume the low-half result, so they escape — which is why this went unnoticed.

One-line fix: select on the target lane's half only.

Root cause

amdgpu_cross_half_shuffle_i32 issues two reads and selects per lane:

i32 self_lane = amdgpu_lane_id();
i32 swapped   = amdgpu_permlane64(value);
i32 byte      = (target_lane & 31) * 4;           // 5-bit ds_bpermute address
...
i32 from_self_half  = amdgpu_ds_bpermute(byte, value);
i32 from_other_half = amdgpu_ds_bpermute(byte, swapped);
return ((target_lane ^ self_lane) & 32) ? from_other_half : from_self_half;   // bug

Because byte masks the lane index to 5 bits, on wave64 the reads are absolute, not lane-relative:

  • from_self_half always resolves to the low SIMD32 (lanes 0..31),
  • from_other_half (via the permlane64 swap) always resolves to the high SIMD32 (lanes 32..63),

regardless of which half the executing lane sits in. The correct payload therefore depends only on which half the target lane lives in — target_lane & 32 — and must not depend on the executing lane.

The old (target_lane ^ self_lane) & 32 is correct for the bottom half (self_lane & 32 == 0) but inverts the choice for the top half: for a same-half target it picks the other-half read. Concretely, identity shuffle(v, i) returns value[i & 31] on lanes 32..63 (e.g. lane 40 reads lane 8), and shuffle_up on the upper half returns lane target_lane - 32 instead of target_lane.

Fix

-  i32 self_lane = amdgpu_lane_id();
   i32 swapped = amdgpu_permlane64(value);
   ...
-  return ((target_lane ^ self_lane) & 32) ? from_other_half : from_self_half;
+  return (target_lane & 32) ? from_other_half : from_self_half;

self_lane becomes unused and is removed.

Testing

Built the runtime bitcode from source and validated with standalone quadrants kernels (no Genesis), single wave64, log2_size=6.

R9700 (gfx1201 / RDNA4) — the affected path:

test before after
identity shuffle(v, i) lanes 32..63 wrong (value[i&31]) 0 wrong lanes
inclusive_add_tiled(6) miscompiles from lane 32 PASS
exclusive_add_tiled(6) miscompiles from lane 32 PASS
reduce_add_tiled(6) PASS (unaffected) PASS

MI300 (gfx942 / CDNA3) — not affected, no regression: on CDNA the ds_bpermute lane mask is the full 64 and permlane64 is patched to identity, so from_self_half == from_other_half and the select is a no-op. Identity / scan / reduce PASS both before and after this change.

Notes / scope

  • The fix relies on ds_bpermute addressing absolute lanes 0..31 for the masked low-31 address on wave64 — confirmed on gfx1201 (RDNA4). If any older RDNA part (gfx10.x) is genuinely SIMD32-relative here, it would need a separate path; I don't have that hardware to check and have flagged it rather than assumed it.
  • This is a correctness fix and is independent of PR [AMDGPU] Don't emit llvm.amdgcn.permlane64 on CDNA #746 (which fixes the CDNA permlane64 codegen crash via a per-arch ds_bpermute lane mask). [AMDGPU] Don't emit llvm.amdgcn.permlane64 on CDNA #746 leaves this RDNA half-select bug in place; the two changes are complementary.

amdgpu_cross_half_shuffle_i32 selected between the same-half and
other-half ds_bpermute reads with (target_lane ^ self_lane) & 32.
Because the ds_bpermute byte address masks the lane to 5 bits,
from_self_half always resolves to the low SIMD32 (lanes 0..31) and
from_other_half (via the permlane64 swap) always resolves to the high
SIMD32 (lanes 32..63), independent of the executing lane. The correct
payload therefore depends only on which half the target lane lives in.

The old self-dependent select was wrong for lanes 32..63: for a target
in the same half it picked the other-half read, so shuffle_up on the
upper half returned lane (target_lane - 32) instead of target_lane,
breaking Hillis-Steele scans on wave64. Select on target_lane & 32.

Validated on R9700 (gfx1201 / RDNA4): identity and scan primitives pass.

@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: b9aecef049

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

i32 from_self_half = amdgpu_ds_bpermute(byte, value);
i32 from_other_half = amdgpu_ds_bpermute(byte, swapped);
return ((target_lane ^ self_lane) & 32) ? from_other_half : from_self_half;
return (target_lane & 32) ? from_other_half : from_self_half;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate target-half selection on RDNA generation

On gfx10/RDNA1-2, which this backend still supports via the has_permlane64 == false LDS fallback in llvm_context.cpp, ds_bpermute in wave64 operates within each 32-lane half rather than as a wave-global low-half read. With this unconditional target-half select, a top-half lane doing an in-half read (for example identity shuffle with self_lane == target_lane == 40) selects from_other_half, so it reads the swapped payload for lane 8 instead of lane 40; top-half cross-half reads are inverted as well. This needs to keep the self-relative selection for those targets or be gated by the actual ds_bpermute semantics.

Useful? React with 👍 / 👎.

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.

@ZJLi2013 thoughts on codex comment?

@hughperkins hughperkins added the awaiting-contributor-action awaiting-contributor-action label Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-contributor-action awaiting-contributor-action

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants