Conversation
Replace the guarded take/1-D-scatter C paths with one loop-nest code generator covering any number of integer index arrays (any ndim, broadcast against each other) mixed with full slices, consecutive or not, on inputs of any stride layout. Bool masks, non-full slices, ignore_duplicates and complex dtypes keep falling back to perform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@ricardoV94 I know CVM is no longer the default so optimizing it is not high priority, but I do have a use case. Namely, numba lacks good profiling tools so whenever I ask AI to try to figure out computational bottlenecks, it uses CVM as proxy - and, invariably first flags indexing as a major source, until it benchmarks with numba to confirm it is not. This PR brings indexing performance to parity with numba in most cases, hopefully making future profiling proxy runs skip that step. |
|
CVM is very different than numba not just indexing (per op dispatch, no major fusion besides Elemwise). When I want to evaluate numba code quality I usually:
Then make use of the two sources to look for obvious inefficiencies. |
I doubt that because these days we rarely do indexing for indexing sake in the numba backend. we either fuse with the elemwise consumer (gather) or producer (scatter) |
Description
AdvancedSubtensorandAdvancedIncSubtensoronly had C implementations for two narrow patterns: a single integer index with every other axis in full (viaPyArray_TakeFrom), andx[idx] (+)= ywith 1-Dx. Everything else ran throughperform, i.e.x.__getitem__andnp.add.at, which is where the time goes in a C-linked function as soon as a model indexes with two arrays, updates a 2-D parameter by row, or takes a gradient throughx[idx].This replaces both with one code generator,
_AdvIndexCGen, that emits a statically unrolled loop nest for the general integer case:x,yand the indices;The loops run in the memory order of the indexed shape, so the output of a gather (and
yof a scatter) is walked sequentially. The innermost loop takes amemcpywhen it is a same-type copy over unit strides and an indexable, vectorizable loop otherwise. Dims and strides are hoisted intoconstlocals up front so stores throughchar*cannot force reloads inside the loops. There is no per-call allocation.Still on
perform: boolean masks, non-full slices inside the advanced op,ignore_duplicates=True(numba keeps numpy's last-write-wins there, which a sequential scatter can't), complex dtypes, and+=into a bool array.yof the scatter may broadcast via static 1-dims, missing leading dims, or as a scalar. A runtime length-1 dim that is not static-broadcastable raises the same "Runtime broadcasting not allowed" error the old 1-D C path raised;performonly checks this for the vector-index case.Timings
ms per call,
float64,int64indices, 20000 index positions,xis(200, 50)or(20, 30, 40). Min of 5 interleaved rounds on a Ryzen 4750U.x[i, j]x[i, j] += yx[i]x[i] += Yx[i].set(Y)x[:, j]x[:, j] += Yx3[i, :, k]x3[i, :, k] += Yx3[:, i]x3[:, i] += Yx[I2, J2](2-D indices)x[I2, J2] += YThe two gathers that already had a C path (
x[i],x3[:, i]) are unchanged within noise; both are bound by writing the 8 MB / 128 MB output.Tests
TestAdvancedIndexingCImplcompares thecvmandpylinkers, asserting the C path is taken, over 15 index patterns × C/F-orderedxfor gathers and × inc/set for scatters (includingybroadcasting), plus dtype mixing (float32 += float64,int32 += float64,boolset from float) and the error paths (out of bounds, index shape mismatch,yshape mismatch, runtime broadcast).tests/tensor/test_subtensor.pyand the subtensor rewrite tests pass underlinker=cvm.Related Issue
Checklist
Type of change
🤖 Generated with Claude Code