Skip to content

Preserve inferred specializations in invokes - #908

Open
maleadt wants to merge 2 commits into
mainfrom
revert-907-revert-899-sds/compilesig_invokes
Open

Preserve inferred specializations in invokes#908
maleadt wants to merge 2 commits into
mainfrom
revert-907-revert-899-sds/compilesig_invokes

Conversation

@maleadt

@maleadt maleadt commented Aug 22, 2026

Copy link
Copy Markdown
Member

Set compilesig_invokes=false so non-inlined calls retain their inferred specialization instead of targeting a widened compilation signature that may not be cached. This avoids unsupported dynamic invokes, including the multidimensional bounds-error paths introduced in Julia 1.14.

For backends without a device heap, provide a null-returning allocator when their runtime module does not define malloc. Valid inputs can then compile and execute even when an error path needs to box an exception; attempted allocations follow the existing out-of-memory path. Backend-provided allocators retain precedence. This addresses #906.

The branch includes the Metal atomic-lowering fix from #904 through its base. Add a Bool-conversion regression with an opaque allocator so LLVM cannot erase the heap-reference stores before AIR lowering, as the usual constant-null test allocator does.

The existing optimization_params(job) hook remains available for backend overrides. Unlike infer_compilation_signature, which also infers widened signatures, disabling compilesig_invokes preserves the specialization selected by inference.

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.48%. Comparing base (78fa65e) to head (c447ad5).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #908      +/-   ##
==========================================
+ Coverage   85.41%   85.48%   +0.07%     
==========================================
  Files          29       29              
  Lines        5582     5582              
==========================================
+ Hits         4768     4772       +4     
+ Misses        814      810       -4     

☔ 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.

Comment thread src/interface.jl
Comment on lines +623 to +624
optimization_params(@nospecialize(job::CompilerJob)) =
CC.OptimizationParams(; compilesig_invokes=false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have to make this configurable And how does this differ from Core.Compiler.infer_compilation_signature?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimization_params is already an overridable hook, no?

And I'm not familiar with the other flag. According to Astra:

Difference from infer_compilation_signature: that flag asks inference to also infer the widened compilation signature. compilesig_invokes=false instead prevents the optimizer from redirecting invokes to that wider specialization. Inferring the wider signature can address a missing cache entry, but does not preserve the concrete argument types needed for static dispatch.

@maleadt
maleadt force-pushed the revert-907-revert-899-sds/compilesig_invokes branch from f898afd to da96c50 Compare September 5, 2026 17:59
@maleadt
maleadt marked this pull request as ready for review September 5, 2026 19:16
@maleadt maleadt changed the title use compilesig_invokes=false Preserve inferred specializations in invokes Sep 6, 2026
Disable compilesig_invokes so noinline vararg calls retain their inferred argument types instead of targeting widened compilation signatures that may not be cached. This is needed for the multidimensional bounds-error paths introduced in Julia 1.14.

Keep the existing optimization_params hook for back-end overrides. Inferring compilation signatures is a different policy: it populates the wider specializations without preserving the precision of the original call.
Use an opaque allocator in the Bool conversion regression so LLVM retains the heap-reference stores that exposed #904. The constant-null test allocator otherwise removes those stores before AIR lowering.
@maleadt
maleadt force-pushed the revert-907-revert-899-sds/compilesig_invokes branch from da96c50 to c447ad5 Compare September 6, 2026 07:24
@maleadt

maleadt commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Why this causes oneAPI.jl failures:

Before, with widened invokes. The optimizer rewrites the invoke target to the constructor's compilation signature. Because of @nospecialize, that is DomainError(::Any, ::Any). Codegen must hand that callee boxed arguments, so it boxes x at the call site, inside throw1:

%box = julia.gc_alloc_obj(8 bytes, Float64)
store double %x, %box
%exc = call DomainError(::Any, ::Any)(%box, %msg)   ; or a jl_invoke if uncached
call jl_throw(%exc)

lower_throw! then replaces the jl_throw with report, signal, and trap, and its hack erases the throw's argument when that argument is an instruction with no other uses. That deletes the constructor call. Now %box has no consumer. Since julia.gc_alloc_obj is an allocation intrinsic whose result nobody reads, LLVM removes it together with the store, before the GC-lowering pass would have turned it into gc_pool_alloc. No gpu_malloc ever appears. The hack's own comment says it was written for exactly this shape: "throwing objects with @nospecialize constructors".

After, with specialized invokes. The invoke keeps the inferred target, the specialization for (Float64, String). That callee takes the double unboxed and returns the struct through an sret pointer, because DomainError is an immutable with two pointer fields. The boxing now happens inside the constructor body, where val is stored into an Any field:

; in throw1
call DomainError_spec(sret %tmp, double %x, %msg)
%obj = julia.gc_alloc_obj(16 bytes, DomainError)
memcpy(%obj, %tmp, 16)
call jl_throw(%obj)

; in DomainError_spec
%box = julia.gc_alloc_obj(8 bytes, Float64)     ; the argument box
store double %val, %box
store %box, %sret[0]

Two things defeat the lowering here. The throw's argument %obj still has a use, the memcpy, so the hack does not erase it. And the constructor call is not connected to the throw through any SSA use at all: its output flows through memory. Even if the outer object were deleted, the box inside the callee is stored into the sret result, so it escapes and LLVM cannot drop it. GC lowering then turns it into gc_pool_alloc, which calls malloc, which on a back-end without one is an unresolved gpu_malloc.

So I think we need to wait until oneAPI.jl gets a device allocator.

Metal.jl was already fixed in #904 and JuliaGPU/Metal.jl#928

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.

3 participants