fix(tabulate): handle empty SE-A neighbors - #5934
Conversation
Treat a zero neighbor capacity as an empty reduction on CPU and GPU, and cover forward and derivative entry points. Coding-Agent: Codex Codex-Version: codex-cli 0.144.6 Model: gpt-5.6-sol Reasoning-Effort: xhigh
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughSE-A CPU and GPU tabulation entrypoints now handle ChangesSE-A empty-neighbor handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #5934 +/- ##
==========================================
- Coverage 79.41% 79.16% -0.25%
==========================================
Files 1071 1071
Lines 124844 124865 +21
Branches 4531 4536 +5
==========================================
- Hits 99144 98850 -294
- Misses 24079 24390 +311
- Partials 1621 1625 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
wanghan-iapcm
left a comment
There was a problem hiding this comment.
The fix is correct and correctly scoped, and the part that took judgement rather than a reflex is right: an early return alone would have been wrong for the forward and second-order paths. out and dz_dy are sized nloc * 4 * last_layer_size independently of nnei, and PyTorch allocates them via torch::empty/empty_like while TF uses allocate_output, so returning without materializing the zero reduction would have handed back uninitialized memory rather than a correct empty result. The first-order outputs genuinely are neighbor-shaped — dy_dem_x is nloc*nnei, dy_dem is nloc*nnei*4, dy_dtwo is nloc*nnei*last_layer_size — so returning ahead of their memsets is safe. I checked dy_dtwo specifically, since a descriptor-shaped output hiding in the gradient path would have made that early return a bug; it mirrors two_embed's neighbor shape at the caller.
Scope is right too. The sentinel read em_x[ii * nnei + nnei - 1] exists only in the SE-A family (tabulate.cc:178,266,367 on master, and the three GPU counterparts). SE-T and SE-R either memset their outputs before looping or write unconditionally, and se_t_tebd already guards nnei_i/nnei_j from #4992, so leaving them alone is correct rather than an omission.
On the test: the existing SE-A tests all use a positive nnei, so the untested cell was nloc > 0 x nnei == 0 — the empty case had only ever been probed on the atom axis, never the neighbor axis. The new test crosses it and genuinely fails pre-fix: compiled against master's tabulate.cc it segfaults on em_x[-1], and passes against this branch. The forward and grad-grad halves also pre-fill with 1.0 and assert exact zeros, so they pin the zeroing behaviour and not merely absence of a crash. That is a real regression test.
Two corrections to the description, neither affecting the code. The origin is not 76a1a2a1d: git show 76a1a2a1d^:source/lib/include/CustomeOperation.h already contains FPTYPE ago = in[ii * nnei + nnei - 1];, so that commit relocated the lookup rather than introducing it. It came from b5166a818 ("model compression", 2021-02-03) three weeks earlier. And "the framework wrappers guard nloc <= 0" is not the case — grep -n "nloc <= 0" source/op/tf/tabulate_multi_device.cc source/op/pt/tabulate_multi_device.cc returns nothing. The nloc guards live in the GPU entry points of the shared lib, added by 8ce9884a3 (#808) for the CUDA launch-configuration constraint that <<<0, ...>>> is an error, not as an input contract; the CPU entry points still have no nloc guard at all. The placement you chose is still the right one — #4992 set that precedent when it added lib-level nnei_i/nnei_j guards for se_e3_tebd — so this only affects the rationale, not the outcome.
One minor thing worth a follow-up rather than a round trip here. In tabulate_fusion_se_a_gpu and tabulate_fusion_se_a_grad_grad_gpu the new nnei <= 0 branch issues gpuMemset and gpuDeviceSynchronize before the DPErrcheck(gpuGetLastError()) that every entry point in this file otherwise runs first. A sticky error left by an earlier kernel would then be reported against this memset and blamed on the empty-neighbor path. It only affects which line gets named in an already-failing program, but moving the guard below the two existing DPErrcheck lines would cost nothing.
Finally, on severity: reaching nnei == 0 from Python needs sum(sel) == 0, i.e. every type excluded, and #5892 notes a bare CUDA probe did not surface a driver error. The out-of-bounds read is real at the library API level regardless — worth fixing as hardening, and the guard is free on the hot path — but I would not describe it as a live crash users are hitting.
Resolve the SE-A test include conflict and keep sticky GPU errors attributed before the empty-neighbor memset paths. Coding-Agent: Codex Codex-Version: codex-cli 0.144.6 Model: gpt-5.6-sol Reasoning-Effort: xhigh
|
Resolved the master conflict and addressed the review mirror note in be42a5f.
Validation: Coding agent: Codex |
d537ad6
Root cause
The shared SE-A CPU and GPU implementations assumed that every local atom had a positive neighbor capacity. They read the last element with an index equivalent to
ii * nnei + nnei - 1before entering the neighbor loop. The framework wrappers guardnloc <= 0, but notnnei <= 0, so a valid empty neighbor axis reaches the native implementation and turns that index into-1.Forward also needs more than an early return because the descriptor does not contain the empty neighbor dimension and may be allocated uninitialized. Its empty reduction is mathematically zero.
Introduction and history
The non-empty-neighbor assumption dates to
76a1a2a1dfrom 2021,optimize interface of custom ops, which introduced the shared tabulation loop and the last-neighbor lookup. Later CUDA optimization and the CUDA/ROCm merge preserved the same contract. This is therefore a long-standing missing edge case, not a recent regression.Test gap
The existing SE-A C++ tests always used a positive
nnei. They covered normal forward and derivative values but never exercisednloc > 0, nnei == 0, so the pre-loop dereference and the need to initialize descriptor-shaped outputs were not visible.Fix
nnei <= 0;Validation
runUnitTests_lib;TestTabulateSeA.empty_neighbors_cpu;TestTabulateSeA.empty_neighbors_gpuon an NVIDIA GeForce RTX 5090 allocation;ruff format .;ruff check ..Fixes #5892
Coding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh
Summary by CodeRabbit
Bug Fixes
Tests