Skip to content

fix(ck-tile): split the TDM dim1 tensor stride at the right bit - #12871

Merged
geyyer merged 5 commits into
developfrom
users/rogeyyer/cktile-tdm-stride-g1
Oct 8, 2026
Merged

geyyer merged 5 commits into
developfrom
users/rogeyyer/cktile-tdm-stride-g1

Conversation

@geyyer

@geyyer geyyer commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Motivation

This PR fixes a bug in TDM descriptor that affects cases with stride values > 65K.

Technical Details

TDM_GROUP1::tensorDim1Stride split the value at bit 32 and masked with 0xFFFFFFFF. The field does not split there:

uint32_t tensor_dim1_stride_lo : 16;   // high half of sgpr6
uint32_t tensor_dim1_stride_hi;        // all of sgpr7

so the split point is bit 16. lo survived only by accidental truncation into its 16-bit bitfield, and hi received bits [32:64], silently dropping bits [16:32] of the stride. The 0xFFFFFFFF mask on a 16-bit field was itself wrong and helped disguise the shift.

dim1 is the only field in the descriptor where lo is narrower than hi. Everywhere else lo >= hi and the symmetric mask/shift is correct -- tensorDim0Stride sits directly above this one and is right -- which is what makes the defect read as correct code.

Also widens tensorDimStride's value parameter to uint64_t. Its only caller passes a uint64_t stride, so the narrow parameter truncated every stride to 32 bits before either setter could place it, making the wider halves of both stride fields unreachable.

Effect: strides below 2^16 were already correct; at and above 2^16 the descriptor pointed the DMA engine at an out-of-range address. That surfaces as a runtime memory fault rather than a wrong result, so no static check catches it.

Adds test_tdm_descriptor_stride, a descriptor readback that packs strides through the real setters on device, copies the raw D# words back, and reconstructs each stride from the bitfield widths. The decoder is written from the field layout rather than as the setters' inverse, so a matching bug in both cannot cancel out. dim0 is carried as a control.

Verified on gfx1250: before this change the dim1 case fails for every stride at or above 2^16 while the dim0 control passes; after it, both pass across the full 48-bit range. A passing GEMM would not have been evidence here -- the TDM GEMM path has independent problems on this part, so "it works now" was not reachable.

Test Plan

Tests in test/ck_tile/tdm/ should pass.

Test Result

Tests are passing.

Submission Checklist

JIRA ID : AICK-2460

TDM_GROUP1::tensorDim1Stride split the value at bit 32 and masked with
0xFFFFFFFF. The field does not split there:

    uint32_t tensor_dim1_stride_lo : 16;   // high half of sgpr6
    uint32_t tensor_dim1_stride_hi;        // all of sgpr7

so the split point is bit 16. `lo` survived only by accidental truncation
into its 16-bit bitfield, and `hi` received bits [32:64], silently dropping
bits [16:32] of the stride. The 0xFFFFFFFF mask on a 16-bit field was itself
wrong and helped disguise the shift.

dim1 is the only field in the descriptor where `lo` is narrower than `hi`.
Everywhere else `lo >= hi` and the symmetric mask/shift is correct --
tensorDim0Stride sits directly above this one and is right -- which is what
makes the defect read as correct code.

Also widens tensorDimStride's `value` parameter to uint64_t. Its only caller
passes a uint64_t stride, so the narrow parameter truncated every stride to
32 bits before either setter could place it, making the wider halves of both
stride fields unreachable.

Effect: strides below 2^16 were already correct; at and above 2^16 the
descriptor pointed the DMA engine at an out-of-range address. That surfaces
as a runtime memory fault rather than a wrong result, so no static check
catches it.

Adds test_tdm_descriptor_stride, a descriptor readback that packs strides
through the real setters on device, copies the raw D# words back, and
reconstructs each stride from the bitfield widths. The decoder is written
from the field layout rather than as the setters' inverse, so a matching bug
in both cannot cancel out. dim0 is carried as a control.

Verified on gfx1250: before this change the dim1 case fails for every stride
at or above 2^16 while the dim0 control passes; after it, both pass across
the full 48-bit range. A passing GEMM would not have been evidence here --
the TDM GEMM path has independent problems on this part, so "it works now"
was not reachable.
@therock-pr-bot

therock-pr-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ⚠️ Warning Error: Source/code files changed without an accompanying unit test.
Expected: add at least one test file named like test_<name>.py / test_<name>.cpp (or <name>_test.*).
Current: code file(s) changed: projects/composablekernel/include/ck_tile/core/arch/amd_tdm_descriptor.hpp, projects/composablekernel/test/ck_tile/tdm/tdm_descriptor_stride.cpp; no test file found
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

therock-pr-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

@geyyer
geyyer marked this pull request as ready for review October 1, 2026 22:03
@geyyer
geyyer requested a review from a team as a code owner October 1, 2026 22:03
Copilot AI balanced review requested due to automatic review settings October 1, 2026 22:03

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.

Copilot review overview

🟡 Changes recommended

The regression test bypasses the widened production dispatcher, leaving part of the fix uncovered.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Fixes 48-bit TDM stride encoding and adds regression coverage.

Changes:

  • Corrects dim1’s split point and preserves 64-bit stride inputs.
  • Adds and registers descriptor readback tests.
File Description
amd_tdm_descriptor.hpp Corrects stride encoding.
tdm_descriptor_stride.cpp Tests descriptor stride packing.
CMakeLists.txt Registers the new test.

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

@JiaLuo-CAN JiaLuo-CAN 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.

LGTM. Please check the minor change requested by Copilot. Thanks.

geyyer and others added 4 commits October 6, 2026 10:26
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make_kernel() only uses the functor's type; the call lives in kentry(),
whose body is `Kernel{}(args...)` only under __HIP_DEVICE_COMPILE__. The
host pass therefore never odr-uses PackStridesKernel::operator(), and the
enclosing anonymous namespace gives it internal linkage, which is exactly
when clang emits -Wunused-member-function. The test targets build with
-Weverything -Werror, so that failed the gfx1250 job.

Mark the member [[maybe_unused]], as DecodeLayoutProbe already does in
test/ck_tile/fmha/test_fmha_bwd_tdm_layout.cpp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@geyyer
geyyer merged commit 74e3b1e into develop Oct 8, 2026
100 checks passed
@geyyer
geyyer deleted the users/rogeyyer/cktile-tdm-stride-g1 branch October 8, 2026 20:39
assistant-librarian Bot pushed a commit to ROCm/composable_kernel that referenced this pull request Oct 8, 2026
fix(ck-tile): split the TDM dim1 tensor stride at the right
 bit (#12871)

## Motivation

This PR fixes a bug in TDM descriptor that affects cases with stride
values > 65K.

## Technical Details

TDM_GROUP1::tensorDim1Stride split the value at bit 32 and masked with
0xFFFFFFFF. The field does not split there:

    uint32_t tensor_dim1_stride_lo : 16;   // high half of sgpr6
    uint32_t tensor_dim1_stride_hi;        // all of sgpr7

so the split point is bit 16. `lo` survived only by accidental
truncation into its 16-bit bitfield, and `hi` received bits [32:64],
silently dropping bits [16:32] of the stride. The 0xFFFFFFFF mask on a
16-bit field was itself wrong and helped disguise the shift.

dim1 is the only field in the descriptor where `lo` is narrower than
`hi`. Everywhere else `lo >= hi` and the symmetric mask/shift is correct
-- tensorDim0Stride sits directly above this one and is right -- which
is what makes the defect read as correct code.

Also widens tensorDimStride's `value` parameter to uint64_t. Its only
caller passes a uint64_t stride, so the narrow parameter truncated every
stride to 32 bits before either setter could place it, making the wider
halves of both stride fields unreachable.

Effect: strides below 2^16 were already correct; at and above 2^16 the
descriptor pointed the DMA engine at an out-of-range address. That
surfaces as a runtime memory fault rather than a wrong result, so no
static check catches it.

Adds test_tdm_descriptor_stride, a descriptor readback that packs
strides through the real setters on device, copies the raw D# words
back, and reconstructs each stride from the bitfield widths. The decoder
is written from the field layout rather than as the setters' inverse, so
a matching bug in both cannot cancel out. dim0 is carried as a control.

Verified on gfx1250: before this change the dim1 case fails for every
stride at or above 2^16 while the dim0 control passes; after it, both
pass across the full 48-bit range. A passing GEMM would not have been
evidence here -- the TDM GEMM path has independent problems on this
part, so "it works now" was not reachable.

## Test Plan

Tests in test/ck_tile/tdm/ should pass.

## Test Result

Tests are passing.

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/TheRock/blob/main/GOVERNANCE.md#pull-requests.

JIRA ID : AICK-2460
jefyang1 added a commit that referenced this pull request Oct 9, 2026
Cross-checked the TDM descriptor against CK's amd_tdm_descriptor.hpp. The field
layout matches exactly -- all 12 TDM_GROUP0 fields and all 20 TDM_GROUP1 fields,
position and width. One value split does not.

tensor_dim1_stride is 48 bits as lo(16) + hi(32): 16 bits in the high half of
sgpr6 plus all of sgpr7. We split it at bit 32, so `hi` received bits [32, 64)
and bits [16, 48) of the stride were dropped. `lo` was right only because a
16-bit field truncates whatever it is given.

    intended 0x10000   ->  encoded 0x0
    intended 0x12345   ->  encoded 0x2345
    intended 0x100000  ->  encoded 0x0

dim1 is the only field in the descriptor where `lo` is narrower than `hi`.
Everywhere else lo >= hi and the symmetric shift is correct -- dim0 sits
directly above it, splits at 32, and is right -- which is what makes the wrong
one read as correct code.

Not reachable before this commit. TDM_MAX_DIM1_STRIDE capped the stride at
0xFFFF, which is exactly the range where both splits agree, and the GEMM wiring
only ever passes 1. Emitted IR is byte-identical to the previous commit for
every TDM parity config; the gate cannot see this change, which is the point of
the new tests.

The cap itself was the deeper error. Both engines carried a comment saying "CK
truncates the dim-1 stride to a 16-bit field", and 827a771's message called
mirroring it deliberate: "Mirrored verbatim rather than corrected -- diverging
from CK's bit layout would be a silent correctness hazard." CK does not
truncate. The field is 48 bits and always was; the old CK code just could not
reach past 16, and CK fixed its own split in #12871 on
2026-10-08. Matching the old behaviour is now the divergence, so the cap is
raised to the real 48-bit bound and dim0 gains the symmetric guard it never had.

Also corrects a units claim while the file is open: the docstring said strides
are "in elements". That holds for dim0. dim1 is in units of dim0_stride, per the
gfx1250 SPG note CK carries in buffer_view.hpp. At rank 2 the only sensible
value is 1, under which both readings agree, so nothing downstream changes --
but it would mislead anyone generalising past rank 2.

Tests, in both engines, mutation-checked by reverting the shift: 4 Python
assertions and 8 C++ checks fail against the old code. They assert the split
point and the lo|hi<<16 round-trip at 0x10000, 0x12345, 2**20 and 2**48-1,
assert dim0's opposite split, and assert that the two halves sit side by side in
word 6 without colliding when both strides are maximal. Every pre-existing test
passed under the defect because none used a stride above 0xFFFF -- the cap saw
to that.

Gate: 69 GREEN / 1 DRIFT at llvm23, the DRIFT being the pre-existing
gfx942_attention_tiled_3d cfg[3]. gemm configs=18, gfx1250_lowering configs=31.
ctest 14/14, host tests 18 in test_tdm (15 before). The two host-suite failures
are byte-identical to HEAD.

This one is verifiable without a GPU -- it is descriptor packing, not emission --
so unlike the TDM wiring it carries no silicon debt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants