Skip to content

Fix integer and complex arguments to unary C math functions - #1051

Open
MaxFreedomPollard wants to merge 2 commits into
inducer:mainfrom
MaxFreedomPollard:fix/c-math-unary
Open

MaxFreedomPollard wants to merge 2 commits into
inducer:mainfrom
MaxFreedomPollard:fix/c-math-unary

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

CMathCallable.with_types computes real_dtype from the argument type at loopy/target/c/init.py:632 and promotes integers to float32 only afterwards, at :634. For an integer argument real_dtype is still the integer type, so none of the float64 / float32 / float128 branches at :639 to :645 match, and control always reaches the raise LoopyTypeError at :647. That fallback prints the already promoted type, so the message is sqrt does not support type <class 'numpy.float32'>, naming a type that is in fact supported. The comparison against the real part of the argument type came in with 0f15b00, "separate pyopencl complex codegen from C99's complex codegen", where the unary branch was still gated on arg_dtypes[0].numpy_dtype.kind in "fc" so an integer could not reach it. The gate did not come along when that code moved into CMathCallable: fd179f4, "Fix bad merge in cmathcallable to fix complex support", already has the ordering this branch repairs, with real_dtype computed above an integer promotion that nothing can reach.

Every unary C math function is affected on CTarget, CudaTarget and ISPCTarget: sqrt, sin, cos, exp, log, abs and the rest. out[i] = sqrt(i) generates code on OpenCLTarget and raises on CTarget, so this is a regression against a sibling target.

The same branch has a second ordering defect. result_dtype is chosen at :649, after name has picked up its f suffix at :642. For a complex64 argument the name is already absf, realf or imagf by then, the name in ["abs", "real", "imag"] test fails, and result_dtype comes out complex64. complex128 takes no suffix and is typed correctly, which is the asymmetry: fmax(abs(a[i]), 1.0) compiles for complex128 and raises fmax does not support complex numbers for complex64, and an auto typed temporary holding abs(a[i]) is declared float complex in the generated C even though cabsf returns a real float.

The change moves the integer promotion above the real_dtype computation and the result_dtype choice above the suffix block. Three smaller repairs follow. The promotion now assigns np.dtype(np.float32) rather than the class np.float32, so the dtype.kind access at :651 is valid on that path; with the class it would have raised AttributeError had the branch ever been reachable. Integer abs is kept integral instead of being promoted, the way OpenCLCallable.with_types already keeps it at loopy/target/opencl.py:253; sending it through float32 would drop bits from an int64 and would make a[abs(5-i)] fail with Non-integral array indices. The two targets still differ in signedness, because OpenCL returns to_unsigned_dtype(dtype) to match OpenCL C 2.2 section 6.13.3 while C's abs, labs and llabs return a signed type, so abs(a[i]) on an int32 yields int32 here and uint32 on OpenCLTarget. C has no unsigned form of abs, so this emits an lpy_abs_<type> preamble in the manner of the lpy_min and lpy_max helpers a few lines above it, with the type name in the tag rather than in the body alone because process_preambles at loopy/codegen/result.py:46 deduplicates on the tag and one kernel can need two widths at once.

Left alone: real, imag and conj of an integer stay out of the promotion and still raise, now naming the argument type instead of float32. Integer min and max are untouched, they are handled by the separate branch at :702. OpenCLTarget and PyOpenCLTarget are untouched.

CTarget, expression      before (8807bdd)                     this branch
sqrt(a[i]), a int32      LoopyTypeError: sqrt ... float32     out[i] = sqrtf((float) (a[i]));
abs(a[i]),  a int64      LoopyTypeError: abs ... float32      out[i] = lpy_abs_int64(a[i]);
a[abs(5-i)]              LoopyTypeError: abs ... float32      out[i] = a[lpy_abs_int32(5 + -1 * i)];
fmax(abs(a[i]), 1.0)     LoopyTypeError: fmax does not        out[i] = fmaxf(cabsf(a[i]), 1.0f);
  a complex64              support complex numbers
<> tmp = abs(a[i])       float complex tmp;                   float tmp;
  a complex64              tmp = cabsf(a[i]);                   tmp = cabsf(a[i]);

Four tests in test/test_target.py, 24 cases in total: test_c_math_integer_argument over sqrt, sin, cos, exp and log on CTarget and CudaTarget; test_c_math_integer_abs over int32, int64 and uint32 on both targets; test_c_math_integer_abs_helper on both targets, covering the array index use and a kernel that needs the int32 and int64 helpers at once; and test_c_math_complex_valued_argument over abs, real and imag for complex64 and complex128, where the complex128 cases are the controls that already passed. They only generate code and infer types, so they need no OpenCL device, the same justification as the test in #1048.

$ LOOPY_NO_CACHE=1 python -m pytest test/test_target.py -q -k c_math
24 passed, 52 deselected in 0.28s
# with only the test file applied to 8807bdd: 21 failed, 3 passed

$ LOOPY_NO_CACHE=1 python -m pytest test/test_target.py test/test_c_execution.py -q
8807bdd:     11 failed, 46 passed, 5 skipped
this branch: 11 failed, 70 passed, 5 skipped

$ LOOPY_NO_CACHE=1 python -m pytest test/test_loopy.py test/test_expression.py -q
both:        38 failed, 202 passed, 3 skipped, 1 xfailed

$ ruff check loopy test
All checks passed!

$ basedpyright loopy test
8807bdd:     76 errors, 27 warnings, 0 notes
this branch: 76 errors, 28 warnings, 0 notes

The pre-existing failures are identical with and without the change, down to the test ids. They are pyopencl._cl.LogicError: clCreateKernel failed: INVALID_KERNEL from Apple's OpenCL on an M3 and are unrelated. The basedpyright error lists match line for line, and the single added warning is Type of "dtype_to_typename" is partially unknown on the target.dtype_to_typename(dtype) call that builds the new preamble. An end to end numeric check through ExecutableCTarget with a real compiler gives abs(int64) -> [0 1 2 3] int64, sqrt(int32) -> [0. 1. 1.4142135 1.7320508] float32 and abs(complex64) -> [0. 1.4142135 2.828427 4.2426405] float32, all matching numpy; the same script on 8807bdd dies at loopy/target/c/init.py:647. Platform: macOS 26.2 (Darwin 25.2.0), Python 3.12.13, numpy 2.5.3, islpy 2026.2.2, pyopencl 2026.1.4, ruff 0.16.7, basedpyright 1.40.1.

CMathCallable.with_types picked the precision suffix from the type of the
argument before integers were promoted to float32, so an integer argument
never matched any of the float branches and always reached the type-error
fallback at loopy/target/c/__init__.py:647, which then printed the promoted
type: "sqrt does not support type <class 'numpy.float32'>". The comparison
against the real part of the type dates to 0f15b00, where the unary branch
still only accepted arguments of kind "f" or "c", so an integer could not
reach it; the guard did not come along when the code moved into the
callable. out[i] = sqrt(i) has therefore worked on OpenCLTarget and failed on
CTarget, CudaTarget and ISPCTarget.

The result type of abs, real and imag was determined after the name had
already picked up its "f" suffix (line 649 vs the suffix block above it), so
"absf" was no longer in the ["abs", "real", "imag"] list and complex64 was
typed as returning complex64. complex128 gets no suffix and was typed
correctly, which is why fmax(abs(a[i]), 1.0) compiled in double precision and
raised "fmax does not support complex numbers" in single precision.

Moving the integer promotion above the computation of the real type and the
result type above the suffix block repairs both. Two further points:

- Integer abs now stays integral, as OpenCLCallable.with_types already keeps
  it. Sending it through float32 would drop bits from an int64 and would make
  abs unusable in an array index. C has abs/labs/llabs but no unsigned form,
  so this emits an lpy_abs_<type> preamble in the manner of the existing
  lpy_min/lpy_max helpers, with the type name in the preamble tag because
  process_preambles deduplicates on the tag and one kernel can need two
  widths.
- The promotion assigned the class np.float32 rather than an np.dtype, so the
  dtype.kind access further down would have raised AttributeError had the
  branch ever been reachable. It now assigns np.dtype(np.float32).

real, imag and conj of an integer are deliberately left out of the promotion
and still raise, now naming the argument type rather than float32.
CFamilyTarget.dtype_to_typename carries no annotations, so basedpyright
reports every call to it as "Type of "dtype_to_typename" is partially
unknown" (reportUnknownMemberType). The three such calls that
CMathCallable.generate_preambles already made are recorded in
.basedpyright/baseline.json, so the call added for the lpy_abs_<type>
preamble is a fourth occurrence, which the checker counts as new and which
fails the basedpyright job.

dtype_to_typename is a one-line forward to
get_dtype_registry().dtype_to_ctype, marked "These kind of shouldn't be
here." where it is defined, while CFamilyTarget.get_dtype_registry and
DTypeRegistry.dtype_to_ctype are both annotated. Going through the registry,
as loopy.target.c.codegen.expression and loopy.target.ispc already do, is the
same call at run time and is fully typed, so the new code needs no baseline
entry. The generated C is unchanged.
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.

1 participant