Skip to content

[Cherry-Pick][Operator Mechanism]Add CUDA small-stride index gradient kernel - #79731

Open
feixi139 wants to merge 10 commits into
PaddlePaddle:release/3.4from
feixi139:fix_index_backward_small_stride_r34
Open

[Cherry-Pick][Operator Mechanism]Add CUDA small-stride index gradient kernel#79731
feixi139 wants to merge 10 commits into
PaddlePaddle:release/3.4from
feixi139:fix_index_backward_small_stride_r34

Conversation

@feixi139

@feixi139 feixi139 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR Category

Operator Mechanism

PR Types

Bug fixes

Description

本 PR 修复 GPU 高级索引(advanced indexing)反向的三个问题:

  1. 低精度 dtype 下重复下标的累加逐步舍入,x[:, idx] 这类表达式的梯度精度明显低于 x[idx]
  2. None(newaxis)出现在高级索引之前时,索引被绑定到错误的轴,前向 shape 就是错的
  3. index_elementwise_get_grad 没有校验 slice_offset 和被索引视图的跨度,非法值会变成越界写。

1. 高级索引反向的低精度累加

torch 的 index_put_(accumulate=True) CUDA 实现完全没有 atomicAdd,只有排序路径,所以它的结果确定、可复现,且重复下标只舍入一次。本节的三处改动合起来把 Paddle 的所有 accumulate 反向也导向这条路径:1.1 放开路由,1.2 补上排序路径缺失的 kernel,1.3 让排序路径能处理放开后新进来的跨步视图。

1.1 路由判据用错了维度,x[:, idx] 落到逐项舍入的 atomicAdd

问题

index_elementwise_get_grad 原来的路由条件是:

if (accumulate && index.size() == 1 && !is_combined) {
  IndexPutWithSortKernel<T, int64_t>(...);
  return;
}

is_combined 来自 pybind:eager_method.ccindex_size = PyTuple_GET_SIZE(index_ptr)__getitem__ 索引元组的元素个数slice / None / Ellipsis 都算),slice_utils.his_combined = (index_size == 1) ? false : true。因此真实判据是「索引元组只有一个元素」:

  • x[idx] → 走排序 kernel(IndexingBackwardKernelStride1 / IndexingBackwardKernel),重复下标在 opmath_t(fp32)寄存器里归约,只在写回时舍入一次;
  • x[:, idx]x[..., idx]x[idx, None]is_combined=true,落到 GPUIndexElementwiseGetGradIndexEleGetGradAccKernel 内是 CudaAtomicAdd(reinterpret_cast<T*>(...), T value),即 fp16/bf16 原生 atomicAdd,每加一次就舍回低精度,误差随重复次数增长,且累加顺序不确定、run-to-run 不可复现。

触发条件是同一下标重复 ≥3 次:N=2 时低精度逐步累加恒等于正确舍入(0+a 精确,a+b 只舍一次),所以必须重复三次以上才会暴露。[16, 8]x[:, idx](索引长 5)200 组随机数据统计:

最大重复次数 fp16 与 torch 不一致率 bf16
1 0/46 0/46
2 0/127 0/127
3 26/27 27/27

改动

路由门从 accumulate && index.size() == 1 && !is_combined 改为 if (accumulate)index_elementwise_get_grad_kernel.cu:724)。放开之后有两类输入是排序路径原来处理不了的,分别由 1.2、1.3 补上。

1.2 排序路径缺 1 < sliceSize <= 32 的 kernel

问题

sliceSize(被索引轴之后的元素个数)落在 1 < sliceSize <= 32 时,即使走到排序路径,也只有 IndexingBackwardKernelStride1(仅 sliceSize == 1)和按 feature 展开的通用 IndexingBackwardKernel 两个选择,后者对每个重复项都做一次 grad_weight 的 read-modify-write,同样是每步舍入。torch 在这里有专门的 indexing_backward_kernel_small_stride,且它真正的分派维度就是 sliceSize 与 warp size 的比较,而不是索引表达式的写法。

改动

新增 IndexingBackwardKernelSmallStride:344),覆盖 1 < sliceSize <= WARP_SIZE:一个线程独占一个 feature 列,该下标的所有重复项在 opmath_t(fp32)寄存器内归约,写回时才舍入一次。分派处(:638)由此按 sliceSize 三分:== 1IndexingBackwardKernelStride1<= WARP_SIZE 走新 kernel,其余走通用 IndexingBackwardKernel

1.3 排序 kernel 无法寻址跨步视图

问题

排序 kernel 把 grad_weight 当作一块平坦 buffer 寻址;原 IndexPutWithSortKernel 自己做 transpose / inverse-perm 并用 funcs::makeLinearIndex 构造线性索引,只在「被索引对象就是整块连续 x_grad」时成立。1.1 放开路由后,x[1::2, idx] 这类先切片再索引也会进来,此时被索引对象是 x_grad 内的一块跨步子区域,直接跑排序 kernel 会写到错误位置。

改动

IndexPutWithSortKernel 改为接收一个描述被索引视图的 SortedPathLayout:401),并直接用 funcs::computeLinearIndex 在该视图上构造线性索引。被索引轴的位置由 layout 给出,且必然是连续的一段,因此 transposeToFrontAndInvPerm 那一步在此路径上不再需要,也省掉了 permute 后的 Contiguous 与写回时的反向 transpose。

视图信息的恢复方式(DeriveSortedPathLayout:437):kernel 参数描述的是 restride 之后的视图 —— restride_src()paddle/fluid/pybind/slice_utils.h)把被索引轴换成广播后的 index shape 并给这些轴零 stride,所以 input_strides 里连续的一段 0 就标出了被索引块,起点即 dims_before;而 index_dims(即 AdvancedIndex::indexed_sizes)开头正是这些轴的 extent,于是视图在索引之前的 shape 及其在 x_grad 内的 stride 都可以还原。据此分三种情况:

  • 视图恰好是整块连续 x_grad 时,直接在 x_grad 上跑排序 kernel;
  • 视图是跨步子区域时(x[1::2, idx]),先分配一个与该视图同形的连续 scratch,在其上归约,再用 StridedCopyKernelview_strides / view_offset 搬回 x_grad —— 这与 torch 用 slice_backward 组合 index_backward 的做法一致。x_grad 已经清零,所以搬回时无需累加。注意 phi::StridedCopyKernel 会覆写 out 的 meta,调用方需自行 save / restore;
  • IsNonOverlapping:411)拒绝自重叠的 as_strided 式视图,这类布局回落到原来的 elementwise 路径(精度更低、run-to-run 不确定),与上游行为保持一致。

slice_offset / index_strides字节偏移(slice_utils.hstride * element_size_bytes),派生 layout 时统一按 elesize 换算回元素单位,不能整除即视为非法并回落。

2. None 出现在高级索引之前导致轴绑定错位

问题

None 位于高级索引之前时,前向 shape 就与 numpy / torch 不一致:

表达式(x[16, 8]i 长 3) numpy / torch 修改前 Paddle
x[None, :, i] (1, 16, 3) (1, 3, 8)
x[:, None, i] (16, 1, 3) (16, 3, 8)
x[None, i] (1, 3, 8) (3, 16, 8)
x[i, None] (3, 1, 8) (3, 1, 8)(正确)

ParseIndex 里的 estimated_dim基础索引输出张量的轴号slice 保留 → ++,整数 → 不 ++(会 decrease),Ellipsis+= rank - specified,bool tensor → += rank),advanced_index_dim[estimated_dim] = estimated_dim 用它记录高级索引落在哪个轴。但 Py_None 分支只 push none_axes不推进 estimated_dim,而 getTensorWithBasicIndexing 结尾的 unsqueeze_ad_func(out, *none_axes) 确实在输出里插入了这个轴(none_axes 已按 decrease_axis 修正,就是基础输出的坐标系)。两个坐标系因此错开 none_count 个位置,索引被绑到错误的轴上。

同分支的单 bool(True / 0-D bool tensor)处理是对的:它 push none_axes 的同时也 estimated_dim++,正好印证 None 分支缺这一步。

静态图 parse_index 有同构的 bug,且更早暴露:replace_none 先把 Noneindices 里剥掉,循环根本看不到 None,修改前静态图 x[None, i] 直接在 gather_nd 报 index out of range。

改动
  • slice_utils.hParseIndex Py_None 分支补 estimated_dim++
  • eager_method.cc 两处 advanced_index_dim 容量从 rank * 2 改为 rank * 2 + index_size(setitem 侧为 + size)。否则 rank 2 张量上 x[None, None, None, None, None, i] 会让 estimated_dim 到 5,发生越界写;
  • variable_index.pyparse_index 改成 _, none_axes = replace_none(indices) 保留原 indices,循环里遇到 Noneestimated_dim += 1; continue,并把 advanced_index 扩容 len(indices)

3. slice_offset 与视图跨度的越界校验

问题

slice_offset 是前向记录的「被索引视图在 x_grad buffer 内的字节偏移」。排序路径和 elementwise 回落路径都会把它加到 x_grad 的基址上,因此一个非法值(负数,或超出 buffer)会直接变成越界写,而不是报错。

同样地,跨步视图是稀疏的,它触达的最远位置由跨度决定而不是元素个数决定,只检查 numel <= grad_numel 不足以保证不越界。

改动
  • IndexElementwiseGetGradKernel 入口校验 0 <= slice_offset < x_grad 字节数
  • DeriveSortedPathLayout 里校验视图触达的最后一个位置 slice_offset / elesize + Σ (view_dims[k] - 1) * view_strides[k] < x_grad->numel(),报错信息带上 offset、view shape 和 strides,便于定位。

修改后的行为

类别 之前 之后
fp16/bf16 x[:, idx] 等重复下标反向 每个重复项舍入一次,误差随重复次数增长,顺序不确定 fp32 寄存器内归约,写回舍入一次,与 torch 逐位一致
1 < sliceSize <= 32 的排序路径 走按 feature 展开的通用 kernel,逐项 read-modify-write 新增 small-stride kernel,单线程独占一列
先切片再高级索引(x[1::2, idx] 走 elementwise atomicAdd 回落 连续 scratch 归约后按视图 stride 搬回
x[None, :, i]None 在前 前向 shape 错、__setitem__ 多数直接报 broadcast 失败、静态图 gather_nd 越界 与 torch 一致
多个连续 None advanced_index_dim 可能越界写 容量按 index_size 扩展
非法 slice_offset / 越界视图 静默越界写 明确报错

自重叠的 as_strided 式视图仍回落到原 elementwise 路径,行为与之前相同。

devPR:#79716

是否引起精度变化

@Paddle-Bot Paddle-Bot 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.

Paddle-Bot Review Board (review完成)

序号 位置 优先级 规则来源 状态
1 非法偏移回退 P1 默认规则
2 负步长读取与归约 P1 默认规则
3 None 轴回归测试 P1 默认规则
4 大索引张量位宽分派 P2 默认规则
5 非连续 value 位宽分派 P2 默认规则
6 CPU 大偏移回归覆盖 P2 默认规则
Powered by Nyanpasu with gpt-5.6-sol 默认推理级别, please check the suggestions carefully.

const size_t ndim = input_dims.size();
const size_t nidx = index_strides.size();
if (nidx == 0 || ndim == 0 || input_strides.size() != ndim ||
index_dims.size() < nidx || slice_offset % elesize != 0) {

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.

P1 这里把未按元素对齐的 slice_offset 判为 false 后仍会继续执行 elementwise 回退,而入口只检查了 0 <= slice_offset < grad_bytes。例如 float32 的 slice_offset=1 会让回退路径构造未对齐的 T*,atomic 写可能越界或损坏相邻元素;index_strides 不能被 elesize 整除时同理。请在进入任一路径前直接拒绝这些非法字节属性,并让跨度校验覆盖回退路径。

std::vector<std::pair<int64_t, int64_t>> axes; // (stride, extent)
for (size_t i = 0; i < dims.size(); ++i) {
if (dims[i] == 1) continue; // its stride is never used
if (strides[i] <= 0) return false;

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.

P1 这里把所有负 stride 都判为重叠布局,但负步长切片本身可以是合法且不重叠的视图。x[::-1, idx] 会因此在后续回落到原来的 CudaAtomicAdd 路径,fp16/bf16 的重复下标仍会逐次舍入且结果不确定,未获得本 PR 的排序归约语义。请基于绝对 stride 判断重叠并正确规范化起始偏移,或提供安全的负 stride 路径,同时补充重复下标回归测试。

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.

P1 上一轮关于负 stride 的问题仍未解决。当前 IsNonOverlapping 虽使用绝对 stride,但前向 index_elementwise_get 仍在 paddle/phi/kernels/gpu/index_elementwise_get_kernel.cu 中以 make_offset_calculator_put<3, false, OffsetT> 构造 offset calculator;其 stride_t 是无符号类型,而 strided_slice 会产生负的 input_strides,因此 x[::-1, idx] 的读取偏移会被转换为巨大正数。排序反向只修正写回,无法挽救已经错误的前向值。请让前向 offset calculator 支持 signed strides 并规范化 base offset,或禁止该路径,并增加负步长 eager/反向回归测试。

// here keeps `advanced_index_dim` in the same coordinate system,
// otherwise a `None` placed before an advanced index would bind the index
// to the wrong axis.
estimated_dim++;

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.

P1 这是本 PR 修复 None 位于高级索引前时轴绑定错误的核心改动,但新增测试只覆盖 CUDA 反向,仓库中也没有 x[None, :, i]x[:, None, i] 等回归。该逻辑同时影响 eager/static 的 getitem 和 setitem,轴号偏差会直接产生静默错误结果;请补充两种模式下与 NumPy 对照的 getitem/setitem 测试,并覆盖多个 None 及被 None 分隔的高级索引。

@codecov-commenter

codecov-commenter commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (release/3.4@7328b32). Learn more about missing BASE report.

Additional details and impacted files
@@               Coverage Diff               @@
##             release/3.4    #79731   +/-   ##
===============================================
  Coverage               ?   100.00%           
===============================================
  Files                  ?         2           
  Lines                  ?         8           
  Branches               ?         0           
===============================================
  Hits                   ?         8           
  Misses                 ?         0           
  Partials               ?         0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Paddle-Bot Paddle-Bot 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.

本轮复核后仍有 1 条 P1 未解决:负步长切片进入前向 index_elementwise_get 时,offset calculator 使用无符号 stride,负 stride 会被解释为巨大正偏移;当前反向排序路径的修正不能弥补前向读取错误。请修复该读取路径或明确禁止此组合并补充回归测试。

Powered by Nyanpasu with gpt-5.6-sol 默认推理级别, please check the suggestions carefully.

Advanced indexing on a reversed view (`x[::-1, idx]`) raised CUDA error 700.
`strided_slice` returns a view whose base offset points at the highest address
of the reversed axis, so the gather/scatter kernels have to reach backwards
from `slice_offset`, but their offset calculators were instantiated with an
unsigned stride type that wrapped those offsets into huge positive values.

- Instantiate the offset calculators of index_elementwise get/put, forward and
  backward, CPU and GPU, with signed strides. Add `funcs::IsInInt32Range` for
  the 32/64-bit dispatch, since the unsigned bound no longer applies, and guard
  the new `CheckOffsetRange` with `if constexpr (signed_strides)` so the
  unsigned instantiations under `stride/` keep their previous behaviour. The
  forward gather kernel now validates both ends of the touched span and reports
  a diagnosable error instead of faulting.
- Order the iteration dimensions in `reorder_dimensions` by |stride| instead of
  by signed value. A negative stride always compared smallest and was sorted
  innermost, and the contiguous strides that `allocate_or_resize_outputs()`
  assigns afterwards then described a permuted layout while the output tensor's
  meta stayed C-contiguous, silently transposing the result for rank >= 3.
  `coalesce_dimensions` keeps its signed comparison on purpose, otherwise a
  reversed axis would be merged into its neighbour.
@Paddle-Bot

Paddle-Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Failed CI看板

流水线名称 问题标签 修复建议 日志片段
Check approval 审批检查失败,发现 2 个 approved errors 【无法判断】补充获取并核对审批检查前序日志中的 2 个具体错误,再决定修复代码或重新运行检查 Job
Clone-linux / Clone Paddle CI 基础设施对象存储签名失败 【与当前改动无关】检查 runner 的对象存储签名配置、请求时间和 endpoint,修复环境后重跑 Clone Paddle Job
Linux-IXUCA / Build and Test 日志获取失败,Job cancelled 【无法判断】当前无可验证日志,重新运行该 Job 或补充完整日志后再定位 Job
Linux-XPU / Test test_index_elementwise 失败,失败重跑成功率低于 50% 【与当前改动有关】优先回退或隔离本 PR 对 index_elementwisestride_utilsslice_utils 和索引解析的改动,补充 XPU 失败断言后修复具体回归 Job
日志分析报告

失败的测试 case:

1. Check approval
   步骤:执行审批检查。
   关键错误:There are 2 approved errors;Process completed with exit code 6。
   具体错误内容未包含在快照中,无法判断对应 PR 改动。

2. Clone-linux / Clone Paddle
   步骤:克隆依赖后执行 BosClient.py 上传对象。
   关键错误:对象存储服务返回 request signature does not match;Process completed with exit code 1。

3. Linux-IXUCA / Build and Test
   步骤:Build and Test Job 被取消。
   关键错误:日志获取失败,gh 返回 HTTP 404。
   无法确认取消原因及具体失败测试。

4. Linux-XPU / Test
   步骤:执行 XPU 测试并重跑失败测试。
   关键错误:test_index_elementwise 失败,重跑成功率低于 50%;Process completed with exit code 8。
   快照未提供断言、堆栈或失败子用例,无法进一步确定具体索引表达式。

根本原因分析:

  • Clone-linux / Clone Paddle 的直接失败点是对象存储请求签名校验失败,发生在 PR 代码编译或测试之前。该错误与本 PR 修改的索引解析、偏移计算和 CUDA/XPU kernel 无直接关联,属于 CI 环境或对象存储请求配置问题。
  • Linux-XPU / Test 的失败测试名为 test_index_elementwise,与本 PR 大范围修改的索引相关代码直接重合。PR 修改了 slice_utils.h 的维度估算、stride_utils.h 的 stride 排序、CPUOffsetCalculator 的 offset 类型,以及多个 GPU index elementwise kernel 的索引分发逻辑,因此该失败应视为当前改动的高相关回归信号。
  • 但快照只给出测试名称和退出码,没有 XPU 断言、输入形状、期望值或堆栈,不能从现有证据确定是 None 与 advanced index 的轴映射问题、负 stride 排序问题、offset 类型变化,还是其他 index elementwise 行为回归。
  • Linux-IXUCA / Build and Test 只有 HTTP 404 的日志获取失败信息。Job 被取消可能与同一 workflow 的失败策略有关,也可能是外部取消;现有证据不足以建立与 XPU 失败或 PR 代码的因果关系。
  • Check approval 仅报告存在 2 个 approved errors,没有对应错误内容。不能据此确认是 PR 新增问题,也不能确认与其他失败 Job 属于同一因果链。

修复建议:

  1. 【无法判断】Check approval:获取完整审批检查日志,定位 2 个 approved errors 的具体规则和文件;若错误来自本 PR,则修复后重新运行审批检查,否则按仓库审批流程处理。
  2. 【与当前改动无关】Clone-linux / Clone Paddle:由 CI 维护者检查对象存储签名请求的 runner 配置、系统时间、区域和 endpoint;环境恢复后重跑 Clone Paddle,无需修改本 PR 索引代码。
  3. 【无法判断】Linux-IXUCA / Build and Test:重新运行该 Job 并保存完整日志;在没有日志前不能判断是否需要修改代码,也不能把取消状态作为独立代码根因。
  4. 【与当前改动有关】Linux-XPU / Test:取得 test_index_elementwise 的首个失败断言和完整测试命令,先用二分方式隔离 slice_utils.hstride_utils.hindex_elementwise.h 及索引解析改动,再针对失败输入补充 XPU 回归测试;修复并确认失败重跑成功率恢复后再合入。

Powered by Nyanpasu with gpt-5.6-luna 默认推理级别, please check the suggestions carefully.

@Paddle-Bot Paddle-Bot 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.

本轮复核确认上一轮负步长读取问题已修复,但发现 1 条新的 P2:大索引张量的位宽分派未纳入第三个 operand,详见内联评论。

Powered by Nyanpasu with gpt-5.6-sol 默认推理级别, please check the suggestions carefully.

if (out->numel() == 0) return;

if (funcs::IsInUint32Range(x.numel() * sizeof(T), out->numel() * sizeof(T))) {
if (funcs::IsInInt32Range(x.numel() * sizeof(T), out->numel() * sizeof(T))) {

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.

P2 现在 OffsetT 使用有符号类型,CheckOffsetRange 会检查第三个 operand(索引张量)的字节跨度,但这里的 32/64 位分派只看 xout,没有覆盖索引张量。例如 xbool[1]、索引为 int64[300000000] 时,x 和输出(300 MB)都在 int32 范围,仍会选择 OffsetT=uint32_t(其 stride_tint32_t),随后第三路 2.4 GB 的 stride 会在构造 calculator 时直接触发溢出;此前无符号 32 位路径仍可表示该范围。请把索引张量的实际 span 纳入分派,或在 32 位检查失败时回退到 64 位,并同步 get/put/grad 的入口。

The indexing offset calculators are instantiated with signed_strides=true,
so their 32 bit fast path is bounded by int32_t rather than uint32_t. The
dispatch, however, only measured x and out. The third operand walks the
index tensors with byte strides scaled by sizeof(int64_t), so it can reach
past int32_t while x and out stay small: a bool x[1] gathered by an int64
index of 3e8 elements produces a 2.4 GB span and CheckOffsetRange rejected
it, even though the previous unsigned path handled that shape.

Add IndexOperandByteSpan(), which derives the broadcast index shape with
cal_shape_stride and returns (elements - 1) * sizeof(int64_t), and feed it
into the dispatch at all six entries that build a signed calculator
(index_elementwise get / get_grad / put / put_grad). The index tensors may
be broadcast, so index[0]->numel() would understate the span and is not
used.

@Paddle-Bot Paddle-Bot 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.

本轮复核确认上一条大索引张量位宽分派问题已修复,但发现 1 条新的 P2:index_elementwise_put_with_tensor 的 32 位分派仍未覆盖非连续 value 的实际 stride span,详见内联评论。

Powered by Nyanpasu with gpt-5.6-sol 默认推理级别, please check the suggestions carefully.

if (funcs::IsInUint32Range(x.numel() * sizeof(T), out->numel() * sizeof(T))) {
if (funcs::IsInInt32Range(x.numel() * sizeof(T),
out->numel() * sizeof(T),
funcs::IndexOperandByteSpan(index_dims))) {

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.

P2 本次已将索引 operand 的跨度纳入分派,但 put_with_tensor 的 offset calculator 还会通过 value.strides() 计算第二路偏移,这里仍只检查 xout 和索引。比如 value 是形状为 [2]、字节 stride 大于 INT32_MAX 的非连续视图时,x/out/index 都可以很小,当前仍选择 32 位,随后 CheckOffsetRange 会因 operand 1 溢出而抛错,尽管 64 位路径能够处理。请按 value 的 dims/strides 计算最大可达偏移并纳入分派,或在 32 位范围检查失败时回退到 64 位。

index_elementwise_put_with_tensor hands `value` to the offset calculator
with its own dims and strides, but the dispatch only weighed x, out and
the index operand. Their extents do not bound value's: a complex128
value of 2e8 elements reaches 3.2e9 bytes while x, out and the int64
index operand (1.6e9 bytes) all stay inside int32_t, so the 32 bit path
was chosen and CheckOffsetRange then rejected a shape the 64 bit path
handles.

@Paddle-Bot Paddle-Bot 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.

本轮复核确认上一条非连续 value 位宽分派问题已修复,但发现 1 条新的 P2:CPU signed offset calculator 没有 64 位回退,会拒绝原先可处理的 2–4 GiB 连续张量,详见内联评论。

Powered by Nyanpasu with gpt-5.6-sol 默认推理级别, please check the suggestions carefully.

strides_vec);
auto offset_calc =
funcs::CPUmake_offset_calculator_put<3>(desired_shape, strides_array);
auto offset_calc = funcs::CPUmake_offset_calculator_put<3, true>(

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.

P2 CPU 路径现在固定使用 CPUOffsetCalculator<..., uint32_t, true>,其 stride_tint32_t,但 CPU 没有 64 位 offset 回退。对于连续 float32 张量,只要可达字节偏移超过 INT32_MAX(约 2 GiB、但仍小于 4 GiB),这里的 CheckOffsetRange 就会直接抛错;改动前的 unsigned calculator 仍可处理这段范围。请保留非负 stride 的 unsigned fast path,或为 CPU 增加 64 位分派。

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.

当前实现已通过统一使用有符号 int64_t offset 修复原问题。不过,上一版提交中用于直接覆盖 INT32_MAX/UINT32_MAX 以上 offset 的轻量 C++ 测试在当前 head 中已被移除,没有对应的边界回归覆盖。请恢复该测试,避免这一位宽修复后续无保护地回退。

CPUOffsetCalculator took a signed_strides flag mirroring the GPU one, and
with it the CPU kernels inherited the GPU's tradeoff: signed offsets make
reversed views work but halve the reach, and the GPU pays for that by
dispatching between a 32 bit and a 64 bit calculator. The CPU has no such
dispatch, so a contiguous float32 [2, 600000000] -- 2.4 GB of reachable
byte offsets, past int32_t but inside uint32_t -- was rejected by
CheckOffsetRange, a shape the previous unsigned instantiation handled.

The tradeoff does not exist on the CPU: a 64 bit add costs the same as a
32 bit one, and only the divmod over linear_idx benefits from index_t
staying 32 bit, which is independent of the offset width. So accumulate
offsets in int64_t unconditionally. That leaves signed_strides with no
false user, so drop the flag from the calculator and its three factories
and revert the five call sites; the reach check goes with it, since int64
bounds cannot be exceeded by a real allocation.
Three places needed the same quantity: the lowest and highest offset that
walking a set of dims with a set of strides can produce.  The bounds check in
GPUIndexElementwiseGetKernel and the layout derivation in
DeriveSortedPathLayout each carried their own loop, and
StridedOperandByteSpan summed |(dim - 1) * stride| for the dispatch, which is
the same hi - lo written differently.  Each copy has to get the same two
details right: a size-1 axis never uses its stride, and a negative stride
pulls the low end below the base instead of raising the high end.

Collect that in OperandReach plus AccumulateReach and have all three use it.
AccumulateReach adds into an existing reach so that operands sharing a base
-- x and the index tensor are both offset from slice_offset -- combine, and
takes a scale so a caller can pass element strides and get bytes.

Also correct the comment above IsNonOverlapping.  It claimed a reversed view
cannot reach the grad kernel because the forward kernel truncates negative
strides to unsigned, but the forward kernel instantiates its offset
calculator with signed_strides = true, so reversed views do arrive here and
the negative case is live.
@feixi139
feixi139 force-pushed the fix_index_backward_small_stride_r34 branch from a8b7ce2 to d81337d Compare September 4, 2026 08:37
@feixi139

feixi139 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

/re-run all-failed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants