Skip to content

emit_into!: compile a closed set of CodeInstances into the caller's module - #19

Draft
vchuravy wants to merge 4 commits into
vc/jc-17-outputfrom
vc/jc-18-emit-into
Draft

vchuravy wants to merge 4 commits into
vc/jc-17-outputfrom
vc/jc-18-emit-into

Conversation

@vchuravy

Copy link
Copy Markdown
Contributor

Stacked on #18. This is the GPUCompiler.jl/Enzyme.jl entry point (plan P4), the JuliaCodegen counterpart of the jl_emit_native call in GPUCompiler's compile_method_instance.

compiled, output = JuliaCodegen.emit_into!(mod, codeinfos; config)
# codeinfos: (CodeInstance, CodeInfo or IRCode) pairs, e.g. from CompilerCaching.get_codeinfos
# compiled[ci] :: CompiledFunctions(func, specfunc)   # jlcall and specsig entry names
# output       :: CodegenOutput                       # literals to bind, symbols, fallbacks

There is also emit_into!(cgm, codeinfos) for an existing CGModule.

  • IR source: each function's IR comes from the set (inflate_ir for a CodeInfo), with its CodeInstance's rettype. :invokes of the set's CodeInstances call their functions.
  • Callees outside the set follow on_unsupported: with :fallback they call native code and are recorded in output.fallbacks; with :error they throw.
  • Always-throwing functions are compiled rather than stubbed. Device code can't call native code.
  • Entries: every CodeInstance gets a jlcall entry and, where specsig_abi handles the signature, a specsig entry. Both have external linkage; the bodies are internal. GPUCompiler reads the same func/specfunc pair from jl_emit_native today.

Supporting changes

  • CGModule(mod, config; ir_source=get_ir, throwing_stubs=true). A callee whose IR the source can't provide now follows the failure policy instead of always becoming a stub. The existing suite is unaffected.
  • CodegenConfig(; gcstack_arg=true, safepoint_on_entry=true) mirrors Julia's CodegenParams. GPUCompiler sets both to false. With gcstack_arg=false, specsig entries have no pgcstack argument and use the C calling convention. @cfunction always uses the runtime's convention, which passes the pgcstack.

Tests (test/emit_into.jl)

They use a small AbstractInterpreter with an overlay method table, standing in for GPUCompiler's, plus a transitive (ci, src) collector.

  • The caller's IR is the one compiled: with the overlay, ei_kernel(3) == 602; native gives 8.
  • Differential corpus through emit_into!: recursion, loops over arrays, error paths, try/catch, aggregates.
  • Entries and linkage: checked, and an always-throwing callee is compiled.
  • GPU-style config: specsig declarations match native code_llvm with CodegenParams(gcstack_arg=false, safepoint_on_entry=false), and the code has no safepoints. The specsig entry is called directly with ccall as a C function.
  • Closed set: a missing callee throws or falls back depending on the policy, and duplicate method instances are rejected.

Known gap: varargs callees (e.g. string interpolation in an error message) are still stubs that call native code. They show up in output.fallbacks.

Tested locally with julia +1.12, +1.13 and +nightly: 6935, 6941 and 6940 tests pass.

🤖 Generated with Claude Code

…odule

The driver for compilers with their own interpreter (GPUCompiler.jl,
Enzyme.jl), as Julia's `jl_emit_native` is for them. It takes `(ci, src)`
pairs, where `src` is the CodeInfo or IRCode the caller inferred.

- Every function's IR comes from the set, with its CodeInstance's return
  type. A callee outside the set follows `on_unsupported`: it calls native
  code, recorded as a fallback, or it throws.
- Functions that always throw are compiled, not stubbed.
- It returns each CodeInstance's jlcall and specsig entries
  (`CompiledFunctions`) and the `CodegenOutput`.

To support this:

- `CGModule` takes the IR source (`ir_source`, by default native inference)
  and `throwing_stubs`.
- A callee whose IR the source can't provide follows the failure policy.
- `CodegenConfig` gains `gcstack_arg` (specsig entries without the pgcstack,
  in the C calling convention) and `safepoint_on_entry`, as Julia's
  `CodegenParams`; GPU targets turn both off. `@cfunction` keeps the
  runtime's convention.

The tests use an interpreter with an overlay method table: its methods are
the ones compiled. They run a differential corpus through `emit_into!`, and
check the specsig declarations against native code with the same
`CodegenParams`, calling the C entry directly, plus missing callees and
always-throwing callees.

Assisted-by: Claude Code (Opus 5.5)
The jlcall and specsig entries call their function with `alwaysinline`,
so that after any pipeline's always-inliner (Julia's, GPUCompiler's), an
entry is the function itself, as native code's are. With GPUCompiler, the
specsig entry of a kernel otherwise stayed a call of the kernel's body.

Assisted-by: Claude Code (Opus 5.5)
The specsig entry of a CodeInstance is `julia_f_N` and its jlcall entry
`jfptr_f_N`, as Julia's code generator names them; the function they
inline is `jlbody_f_N`. GPUCompiler's tests, and whoever else matches the
names, see the same functions with either code generator.

Assisted-by: Claude Code (Opus 5.5)
Functions that other functions call keep native code's name `julia_f_N`,
as calls that aren't inlined refer to them; the specsig entry is `j_f_N`,
the prefix of native code's specsig wrappers.

Assisted-by: Claude Code (Opus 5.5)
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