Skip to content

Fix ISPC code generation for subscripted private temporaries - #1057

Merged
inducer merged 1 commit into
inducer:mainfrom
MaxFreedomPollard:fix/ispc-private-array-subscript
Oct 28, 2026
Merged

inducer merged 1 commit into
inducer:mainfrom
MaxFreedomPollard:fix/ispc-private-array-subscript

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

ExprToISPCExprMapper.map_subscript calls self.kernel.get_grid_size_upper_bounds_as_exprs() with no arguments (loopy/target/ispc.py:172). callables_table has been a required parameter of that method since the kernel callables work in 2021 (loopy/kernel/__init__.py:1108), so ISPC code generation fails for any kernel that subscripts a private temporary:

TypeError: LoopKernel.get_grid_size_upper_bounds_as_exprs() missing 1 required positional argument: 'callables_table'

A kernel does not need an explicitly private array to hit this. An ilp iname privatizes the temporaries in its loop into arrays, so this fails the same way on 067ffc8:

knl = lp.make_kernel(
    "{[i]: 0<=i<n}",
    """
    <> tmp = 2*a[i]
    out[i] = tmp + 1
    """,
    [lp.GlobalArg("a,out", np.float32, shape="n"), lp.ValueArg("n", np.int32)],
    target=lp.ISPCTarget(), lang_version=(2018, 2))
knl = lp.split_iname(knl, "i", 32, outer_tag="g.0")
knl = lp.split_iname(knl, "i_inner", 4, outer_tag="l.0", inner_tag="ilp")
lp.generate_code_v2(knl)

The other three calls to this method in loopy/target/ispc.py already pass the table: map_variable since fbdda4d (#764, the scalar case from #763), the private temporary declarator since 34c7344, and pre_codegen_entrypoint_check. This call was missed. The change passes self.codegen_state.callables_table, as map_variable does twenty lines above. basedpyright flags the call as reportCallIssue ("Argument types: ()"), but that entry sits in .basedpyright/baseline.json.

With the fix, the accesses match the declaration that get_temporary_var_declarator emits, with one copy of the temporary per program instance (lsize + shape). For the kernel in the new test (tmp[j] private, i split by 8 into l.0 and g.0):

float tmp[8 * 4];
...
tmp[programIndex + 8 * (0 + j)] = 2.0 * a[4 * (varying int32) programIndex + 32 * ((uniform int32) taskIndex0) + j];
out[4 * (varying int32) programIndex + 32 * ((uniform int32) taskIndex0) + j] = tmp[programIndex + 8 * (0 + 3 + -1 * j)];

The ilp example above now generates float tmp[8 * 4];, with the unrolled accesses tmp[programIndex + 8 * (0 + 0)] through tmp[programIndex + 8 * (0 + 3)].

test_ispc_private_array in test/test_target.py is the subscripted counterpart of test_ispc_private_var. It checks the declaration and that both the store and the load are offset by programIndex. Like the other ISPC tests it only generates code, so it needs no ISPC compiler and no OpenCL device. I did not compile the generated ISPC, because no ISPC compiler was available here.

Verified on macOS 26.7.1 (arm64), Python 3.12.15, numpy 2.5.4, islpy 2026.2.2, namedisl 2026.1.1, pymbolic 2025.1, pyopencl 2026.1.4. This machine has no usable OpenCL device, so a local pytest plugin made pyopencl's fixture report zero devices, and the tests that need a context are skipped:

$ LOOPY_NO_CACHE=1 python -m pytest test/test_target.py -k ispc -q
4 passed, 49 deselected                    # this branch
1 failed, 3 passed, 49 deselected          # loopy/target/ispc.py reverted to 067ffc8: test_ispc_private_array
                                           # fails with the TypeError above at loopy/target/ispc.py:172

$ LOOPY_NO_CACHE=1 python -m pytest test/test_target.py test/test_loopy.py -q
1 failed, 98 passed, 134 skipped           # 067ffc8
1 failed, 99 passed, 134 skipped           # this branch

The per-test outcomes are the same on both trees except for the new test. The one failure, test_dgemm_with_rectangular_tile_prefetch, calls cl.create_some_context() itself and gets clGetDeviceIDs failed: INVALID_VALUE on both trees.

$ uv run --only-dev ruff check                       # ruff 0.17.2
All checks passed!

$ typos loopy/target/ispc.py test/test_target.py     # typos-cli 1.51.1
(no findings)

$ basedpyright loopy test                            # basedpyright 1.40.2
163 errors on 067ffc8 and on this branch. The error lists are identical apart from line numbers in loopy/target/ispc.py.

🤖 Generated with Claude Code

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor Author

The basedpyright failure is in loopy/symbolic.py, which this PR doesn't touch; it fails the same way on main (067ffc8).

ExprToISPCExprMapper.map_subscript calls
get_grid_size_upper_bounds_as_exprs() without the callables table, which
has been a required argument since the 2021 kernel callables work. Any
kernel that subscripts a private temporary therefore fails ISPC code
generation with "TypeError: LoopKernel.get_grid_size_upper_bounds_as_exprs()
missing 1 required positional argument: 'callables_table'". That includes
ordinary uses of an "ilp" iname, which privatizes temporaries into arrays.

The other three calls in loopy/target/ispc.py were updated over time
(map_variable in fbdda4d for inducergh-763, the temporary declarator in
34c7344); this one was missed. Pass self.codegen_state.callables_table,
as map_variable does.

test_ispc_private_array is the subscripted counterpart of
test_ispc_private_var.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@inducer
inducer force-pushed the fix/ispc-private-array-subscript branch from 0ebc43f to 20ecfc0 Compare October 28, 2026 20:31
@inducer
inducer merged commit d80feec into inducer:main Oct 28, 2026
17 checks passed
@inducer

inducer commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Thanks!

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.

2 participants