Repository navigation
[WS1][CUDA][MiniMax-H3] adaln_gate_residual: gated residual with in-kernel gate gather (RFC #420 stack 6/10) #488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
maxiaosong1124
merged 46 commits into
RL-Align:test-h3
from
fusheng-ji:feat/h3-adaln-gate-residual
Oct 30, 2026
Merged
Changes from all commits
Commits
Show all changes
46 commits
Select commit
Hold shift + click to select a range
e7bc53a
[WS1][CUDA][MiniMax-H3] adaln_gate_residual: gated residual with in-k…
fusheng-ji cd688b3
[WS1][MiniMax-H3] adaln_gate_residual: B200 evidence report and figure
fusheng-ji debde49
Merge inherited RFC 420 review fixes into PR #488
fusheng-ji 7090f00
fix(h3): address native validation and evidence review comments
fusheng-ji a05952f
Merge inherited RFC 420 review fixes into PR #488
fusheng-ji be7c6a9
Merge the H3 prior-art runner into PR #488
fusheng-ji b4c9fd5
Merge the H3 prior-art runner fix into PR #488
fusheng-ji 052f5cb
Merge the H3 prior-art figure fix into PR #488
fusheng-ji 5f4253d
Merge the H3 prior-art figure fix into PR #488
fusheng-ji dd35c14
docs(h3): gate_residual vs existing implementations (B200, 4010854)
fusheng-ji 2d1b9d0
Merge the H3 prior-art evidence into PR #488
fusheng-ji 1d1e739
Merge the H3 prior-art figure layout fix into PR #488
fusheng-ji c3fad21
Merge test-h3 refactor and migrate H3 kernels, tools, and tests
fusheng-ji fdc58d4
Merge migrated H3 dependency PR #487
fusheng-ji 6682e76
Merge migrated validation documentation from PR #487
fusheng-ji f75ff23
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji a746bff
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 3bbcca4
evidence(h3): gate_residual direct bitwise comparison on B200
fusheng-ji 3310212
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 2c27639
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 9c76b32
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 81741e9
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji d8a4a69
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 9592623
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 9b95c4a
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji a8e5901
docs: move the measurement outputs out of the pull request
fusheng-ji 3801808
docs: point commit hashes at the rewritten history
fusheng-ji 1cacd95
Merge test-h3 into feat/h3-adaln-gate-residual
fusheng-ji c08d9ee
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 9c9ab15
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 669513d
fix(h3): keep the gate-residual forward out of the timed backward
fusheng-ji 8e5ae90
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji e5318ad
fix(h3): do not ask for the checkpoint when the gate-residual evidenc…
fusheng-ji 7ed723a
Merge feat/h3 parent (#487) into #488
fusheng-ji 4cde743
Merge parent (#487) into #488
fusheng-ji 37f54db
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 239ef80
fix(h3): assert the gate row in the native kernels and check the back…
fusheng-ji a2d0674
docs(h3): gate-residual latencies from the backward-only rerun
fusheng-ji d46cbe5
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 1857fa6
fix(h3): check the gate backward's tile metadata and lengths, and sca…
fusheng-ji 53ba838
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji 2084242
Merge feat/h3-rmsnorm into feat/h3-adaln-gate-residual
fusheng-ji ab407b3
fix(h3): reject gate tables whose rows overlap, and state the residua…
fusheng-ji d20d767
Merge #487 round-5 fixes into #488
fusheng-ji 60e7785
fix(h3): return no residual gradient for a residual that needs none
fusheng-ji 22a3c79
Merge upstream test-h3 into feat/h3-adaln-gate-residual
fusheng-ji File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2026 RL-Kernel Contributors | ||
| // | ||
| // MiniMax-H3 gated residual (RFC #420 `adaln_gate_residual`). | ||
| // | ||
| // out[r, j] = cast(residual[r, j] + cast(gate[i, j] * y[r, j])) i = index[r % S] | ||
| // | ||
| // The gate row is read from a strided view of the AdaLN table (gate_msa or | ||
| // gate_mlp) inside the kernel, and the two roundings sit exactly where the | ||
| // eager `residual + gate.index_select(0, i) * y` rounds (__fmul_rn/__fadd_rn | ||
| // keep FP32 from contracting them into one FMA), so the result is | ||
| // bitwise equal to diffusers. Elementwise: every output depends only on its | ||
| // own inputs. | ||
| // | ||
| // Backward: d_residual = grad; d_y = cast(grad * gate) (the product of two | ||
| // 16-bit values is exact in FP32, so this equals the eager VJP bitwise); | ||
| // d_gate[i] = sum over positions mapped to i of grad * y, an FP32 segmented | ||
| // sum over positions sorted stably by row, in fixed tiles folded in order. | ||
|
|
||
| #include <torch/extension.h> | ||
| #include <ATen/cuda/CUDAContext.h> | ||
| #include <c10/cuda/CUDAGuard.h> | ||
| #include <c10/cuda/CUDAException.h> | ||
| #include <cuda_bf16.h> | ||
| #include <cuda_fp16.h> | ||
|
|
||
| #include <cstdint> | ||
| #include <vector> | ||
|
|
||
| #include "tile_metadata.cuh" | ||
|
|
||
| namespace { | ||
|
|
||
| __device__ __forceinline__ float to_f(float v) { return v; } | ||
| __device__ __forceinline__ float to_f(__nv_bfloat16 v) { return __bfloat162float(v); } | ||
| __device__ __forceinline__ float to_f(__half v) { return __half2float(v); } | ||
|
|
||
| template <typename T> | ||
| __device__ __forceinline__ T from_f(float v); | ||
| template <> | ||
| __device__ __forceinline__ float from_f<float>(float v) { return v; } | ||
| template <> | ||
| __device__ __forceinline__ __nv_bfloat16 from_f<__nv_bfloat16>(float v) { return __float2bfloat16(v); } | ||
| template <> | ||
| __device__ __forceinline__ __half from_f<__half>(float v) { return __float2half(v); } | ||
|
|
||
| template <typename T> | ||
| __device__ __forceinline__ float round_t(float v) { return to_f(from_f<T>(v)); } | ||
|
|
||
| struct Gate { | ||
| const void* rows; // (R, N) view, row stride `stride` | ||
| int64_t stride; | ||
| const int64_t* index; // (S,) | ||
| int64_t seq; | ||
| int64_t num_rows; | ||
| }; | ||
|
|
||
| // One block per row (grid-strided); 16-byte vectors when the row allows it. | ||
| // `kVecElems` elements are loaded together; each is computed independently. | ||
| template <typename T, bool kDy> | ||
| __global__ void gate_residual_rows_kernel(const T* __restrict__ a, const T* __restrict__ b, | ||
| T* __restrict__ out, int64_t rows, int64_t n, | ||
| Gate gate, bool vectorized) { | ||
| constexpr int kVecElems = 16 / sizeof(T); | ||
| for (int64_t r = blockIdx.x; r < rows; r += gridDim.x) { | ||
| const int64_t i = gate.index[r % gate.seq]; | ||
| CUDA_KERNEL_ASSERT(i >= 0 && i < gate.num_rows); | ||
| const T* g = static_cast<const T*>(gate.rows) + i * gate.stride; | ||
| const T* ar = a + r * n; | ||
| const T* br = kDy ? nullptr : b + r * n; | ||
| T* orow = out + r * n; | ||
| if (vectorized) { | ||
| for (int64_t v = threadIdx.x; v < n / kVecElems; v += blockDim.x) { | ||
| const uint4 av = reinterpret_cast<const uint4*>(ar)[v]; | ||
| const uint4 gv = reinterpret_cast<const uint4*>(g)[v]; | ||
| uint4 bv = av; | ||
| if (!kDy) bv = reinterpret_cast<const uint4*>(br)[v]; | ||
| const T* ae = reinterpret_cast<const T*>(&av); | ||
| const T* ge = reinterpret_cast<const T*>(&gv); | ||
| const T* be = reinterpret_cast<const T*>(&bv); | ||
| uint4 ov; | ||
| T* oe = reinterpret_cast<T*>(&ov); | ||
| #pragma unroll | ||
| for (int k = 0; k < kVecElems; ++k) { | ||
| if (kDy) { | ||
| oe[k] = from_f<T>(to_f(ae[k]) * to_f(ge[k])); // grad * gate | ||
| } else { | ||
| const float p = round_t<T>(__fmul_rn(to_f(ge[k]), to_f(be[k]))); // gate * y | ||
| oe[k] = from_f<T>(__fadd_rn(to_f(ae[k]), p)); // residual + p | ||
| } | ||
| } | ||
| reinterpret_cast<uint4*>(orow)[v] = ov; | ||
| } | ||
| } else { | ||
| for (int64_t j = threadIdx.x; j < n; j += blockDim.x) { | ||
| if (kDy) { | ||
| orow[j] = from_f<T>(to_f(ar[j]) * to_f(g[j])); | ||
| } else { | ||
| const float p = round_t<T>(__fmul_rn(to_f(g[j]), to_f(br[j]))); | ||
| orow[j] = from_f<T>(__fadd_rn(to_f(ar[j]), p)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // partial[tile, j] = sum over sorted positions of the tile (ascending) of grad * y | ||
| template <typename T> | ||
| __global__ void gate_grad_partial_kernel(const T* __restrict__ grad, const T* __restrict__ y, | ||
| const int64_t* __restrict__ sorted_pos, | ||
| const int64_t* __restrict__ tile_begin, | ||
| const int64_t* __restrict__ tile_end, | ||
| float* __restrict__ partial, int64_t n) { | ||
| const int64_t tile = blockIdx.x; | ||
| const int64_t j = static_cast<int64_t>(blockIdx.y) * blockDim.x + threadIdx.x; | ||
| if (j >= n) return; | ||
| float acc = 0.0f; | ||
| for (int64_t p = tile_begin[tile]; p < tile_end[tile]; ++p) { | ||
| const int64_t e = sorted_pos[p] * n + j; | ||
| acc = fmaf(to_f(grad[e]), to_f(y[e]), acc); | ||
| } | ||
| partial[tile * n + j] = acc; | ||
| } | ||
|
|
||
| template <typename T> | ||
| __global__ void gate_grad_fold_kernel(const float* __restrict__ partial, | ||
| const int64_t* __restrict__ seg_first_tile, | ||
| T* __restrict__ out, int64_t n) { | ||
| const int64_t seg = blockIdx.x; | ||
| const int64_t j = static_cast<int64_t>(blockIdx.y) * blockDim.x + threadIdx.x; | ||
| if (j >= n) return; | ||
| float acc = 0.0f; | ||
| for (int64_t t = seg_first_tile[seg]; t < seg_first_tile[seg + 1]; ++t) acc += partial[t * n + j]; | ||
| out[seg * n + j] = from_f<T>(acc); | ||
| } | ||
|
|
||
| #define H3_DISPATCH(TYPE, NAME, ...) \ | ||
| [&] { \ | ||
| switch (TYPE) { \ | ||
| case at::kFloat: { using T = float; __VA_ARGS__(); break; } \ | ||
| case at::kHalf: { using T = __half; __VA_ARGS__(); break; } \ | ||
| case at::kBFloat16: { using T = __nv_bfloat16; __VA_ARGS__(); break; } \ | ||
| default: TORCH_CHECK(false, NAME, ": unsupported dtype ", TYPE); \ | ||
| } \ | ||
| }() | ||
|
|
||
| Gate make_gate(const torch::Tensor& gate, const torch::Tensor& index, const torch::Tensor& x) { | ||
| TORCH_CHECK(gate.is_cuda() && gate.device() == x.device() && gate.scalar_type() == x.scalar_type(), | ||
| "gate must be a CUDA tensor with the activations' dtype"); | ||
| TORCH_CHECK(gate.dim() == 2 && gate.size(1) == x.size(1) && gate.stride(1) == 1, | ||
| "gate must be (R, N) with unit column stride"); | ||
| // Rows are read as dense N-element spans at `stride(0)`: a view of a wider table is fine, an | ||
| // expanded or overlapping one (stride(0) < N) is not a table of rows. | ||
| TORCH_CHECK(gate.size(0) <= 1 || gate.stride(0) >= gate.size(1), "gate row stride ", | ||
| gate.stride(0), " is smaller than N=", gate.size(1)); | ||
| TORCH_CHECK(index.is_cuda() && index.device() == x.device() && index.scalar_type() == at::kLong && | ||
| index.dim() == 1 && index.is_contiguous() && index.numel() > 0, | ||
| "index must be a non-empty contiguous int64 tensor on ", x.device()); | ||
| TORCH_CHECK(x.size(0) % index.size(0) == 0, "rows must be a multiple of S"); | ||
| return Gate{gate.data_ptr(), gate.stride(0), index.data_ptr<int64_t>(), index.size(0), gate.size(0)}; | ||
| } | ||
|
|
||
| void check_act(const torch::Tensor& t, const char* name, const torch::Tensor& like) { | ||
| TORCH_CHECK(t.is_cuda() && t.device() == like.device() && t.scalar_type() == like.scalar_type(), | ||
| name, " must match the residual's device and dtype"); | ||
| TORCH_CHECK(t.sizes() == like.sizes() && t.is_contiguous(), name, | ||
| " must be contiguous with the residual's shape"); | ||
| } | ||
|
|
||
| unsigned row_blocks(int64_t rows) { | ||
| return static_cast<unsigned>(std::min<int64_t>(rows, 1 << 30)); | ||
| } | ||
|
|
||
| bool can_vectorize(const torch::Tensor& gate, int64_t n, std::initializer_list<const torch::Tensor*> acts) { | ||
| const int64_t vec = 16 / gate.element_size(); | ||
| if (n % vec != 0 || (gate.stride(0) * gate.element_size()) % 16 != 0) return false; | ||
| if (reinterpret_cast<uintptr_t>(gate.data_ptr()) % 16 != 0) return false; | ||
| for (const auto* t : acts) { | ||
| if (reinterpret_cast<uintptr_t>(t->data_ptr()) % 16 != 0) return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| // residual, y: (M, N); gate: (R, N) view; index: (S,), M % S == 0. | ||
| torch::Tensor h3_gate_residual_forward(torch::Tensor residual, torch::Tensor y, torch::Tensor gate, | ||
| torch::Tensor index) { | ||
| TORCH_CHECK(residual.is_cuda() && residual.dim() == 2 && residual.is_contiguous() && | ||
| residual.numel() > 0, | ||
| "residual must be a non-empty contiguous (M, N) CUDA tensor"); | ||
| check_act(y, "y", residual); | ||
| const Gate g = make_gate(gate, index, residual); | ||
| const c10::cuda::CUDAGuard guard(residual.device()); | ||
| auto out = torch::empty_like(residual); | ||
| auto stream = at::cuda::getCurrentCUDAStream(); | ||
| const bool vec = can_vectorize(gate, residual.size(1), {&residual, &y, &out}); | ||
| H3_DISPATCH(residual.scalar_type(), "h3_gate_residual_forward", [&] { | ||
| gate_residual_rows_kernel<T, false><<<row_blocks(residual.size(0)), 256, 0, stream>>>( | ||
| reinterpret_cast<const T*>(residual.data_ptr()), reinterpret_cast<const T*>(y.data_ptr()), | ||
| reinterpret_cast<T*>(out.data_ptr()), residual.size(0), residual.size(1), g, vec); | ||
| }); | ||
| C10_CUDA_KERNEL_LAUNCH_CHECK(); | ||
| return out; | ||
| } | ||
|
|
||
| // Returns {d_y, d_gate (R, N) in the gate dtype}. d_residual is the incoming grad. | ||
| std::vector<torch::Tensor> h3_gate_residual_backward(torch::Tensor grad, torch::Tensor y, | ||
| torch::Tensor gate, torch::Tensor index, | ||
| torch::Tensor sorted_pos, | ||
| torch::Tensor tile_begin, | ||
| torch::Tensor tile_end, | ||
| torch::Tensor seg_first_tile) { | ||
| TORCH_CHECK(grad.is_cuda() && grad.dim() == 2 && grad.is_contiguous(), | ||
| "grad must be a contiguous (M, N) CUDA tensor"); | ||
| check_act(y, "y", grad); | ||
| const Gate g = make_gate(gate, index, grad); | ||
| for (const auto* t : {&sorted_pos, &tile_begin, &tile_end, &seg_first_tile}) { | ||
| TORCH_CHECK(t->is_cuda() && t->device() == grad.device() && t->scalar_type() == at::kLong && | ||
| t->dim() == 1 && t->is_contiguous(), | ||
| "tile metadata must be contiguous int64 tensors on ", grad.device()); | ||
| } | ||
| const int64_t rows = grad.size(0); | ||
| const int64_t n = grad.size(1); | ||
| TORCH_CHECK(seg_first_tile.numel() >= 1, "seg_first_tile must hold R + 1 entries"); | ||
| const int64_t segments = seg_first_tile.numel() - 1; | ||
| TORCH_CHECK(segments == gate.size(0), "segments must equal the gate rows"); | ||
| TORCH_CHECK(sorted_pos.numel() == rows, "sorted_pos must have M entries"); | ||
| TORCH_CHECK(tile_begin.numel() == tile_end.numel(), "tile_begin/tile_end length mismatch"); | ||
| const int64_t tiles = tile_begin.numel(); | ||
| const c10::cuda::CUDAGuard guard(grad.device()); | ||
| auto stream = at::cuda::getCurrentCUDAStream(); | ||
| auto dy = torch::empty_like(y); | ||
| auto partial = torch::empty({std::max<int64_t>(tiles, 1), n}, grad.options().dtype(at::kFloat)); | ||
| auto dgate = torch::empty({segments, n}, grad.options()); | ||
| const int threads = 256; | ||
| const int64_t col_blocks64 = (n + threads - 1) / threads; | ||
| TORCH_CHECK(col_blocks64 <= 65535, "N=", n, " needs ", col_blocks64, | ||
| " column blocks, more than the grid.y limit of 65535"); | ||
| const unsigned col_blocks = static_cast<unsigned>(col_blocks64); | ||
| // The tile list is caller-built metadata: check it before the kernels read through it. | ||
| h3_tiles::check_tile_metadata(stream, sorted_pos.data_ptr<int64_t>(), rows, | ||
| tile_begin.data_ptr<int64_t>(), tile_end.data_ptr<int64_t>(), tiles, | ||
| seg_first_tile.data_ptr<int64_t>(), segments); | ||
| H3_DISPATCH(grad.scalar_type(), "h3_gate_residual_backward", [&] { | ||
| const T* gp = reinterpret_cast<const T*>(grad.data_ptr()); | ||
| const T* yp = reinterpret_cast<const T*>(y.data_ptr()); | ||
| const bool vec = can_vectorize(gate, n, {&grad, &dy}); | ||
| gate_residual_rows_kernel<T, true><<<row_blocks(rows), threads, 0, stream>>>( | ||
| gp, nullptr, reinterpret_cast<T*>(dy.data_ptr()), rows, n, g, vec); | ||
| if (tiles > 0) { | ||
| gate_grad_partial_kernel<T><<<dim3(static_cast<unsigned>(tiles), col_blocks), threads, 0, stream>>>( | ||
| gp, yp, sorted_pos.data_ptr<int64_t>(), tile_begin.data_ptr<int64_t>(), | ||
| tile_end.data_ptr<int64_t>(), partial.data_ptr<float>(), n); | ||
| } | ||
| if (segments > 0) { // a zero-sized grid is not a valid launch | ||
| gate_grad_fold_kernel<T><<<dim3(static_cast<unsigned>(segments), col_blocks), threads, 0, stream>>>( | ||
| partial.data_ptr<float>(), seg_first_tile.data_ptr<int64_t>(), | ||
| reinterpret_cast<T*>(dgate.data_ptr()), n); | ||
| } | ||
| }); | ||
| C10_CUDA_KERNEL_LAUNCH_CHECK(); | ||
| return {dy, dgate}; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tile metadata is used without checking that the terminal segment index stays within
partial's first dimension.gate_grad_fold_kernelreadsseg_first_tile[seg + 1]and loopst < seg_first_tile[seg + 1], indexingpartial[t * n + j], but nothing verifiesseg_first_tile[segments] == tiles(nor thatsorted_pos.numel() == rows, nortile_begin/tile_endbounds).h3_rmsnorm_backwardperforms these checks; this entry point does not, so a caller passing mismatched metadata (the tile tensors are a separate public parameter list) reads out of bounds ofpartialand silently produces a wrongd_gate. Add the missingTORCH_CHECKs here, mirroring the rmsnorm entry point.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not changed in the kernels. The tile metadata is not a public input: it is built by
_segment_tilesfrom the same index tensor (int64sorted_pos,tile_begin,tile_end,seg_first_tile, non-decreasing by construction,seg_first_tile[R] == tiles) and the native entry points check dtype, rank, contiguity, device, sizes and, since 0f67234, a non-emptyseg_first_tile. Validating the values on the host would need a device-to-host read of every tensor on each backward, and the backward tests compare the result with the golden. A caller that builds metadata by hand is outside the contract.