Repository navigation
Conversation
… them The IR of `Base.llvmcall` gets `Ptr` arguments, and returns `Ptr` results, as integers of the pointer's size (`inttoptr i64 %0`), as Julia's code generator passes them. They were `ptr`s, so IR written for native code (GPU code's, GPUCompiler's tests) didn't parse. Pointers and integers are converted with ptrtoint/inttoptr. `ccall(..., llvmcall, ...)` keeps pointers. Assisted-by: Claude Code (Opus 5.5)
…e name
- `ccall("extern name", llvmcall, ...)` calls the external function `name`, as
native code does; GPU runtimes call their functions so (`malloc`, AIR's
builtins, `deferred_codegen`).
- The base name of an overloaded intrinsic (`llvm.powi`) is the overload of
the call's signature (`llvm.powi.f64.i32`), as native code mangles it,
with LLVM.jl's `Function(mod, intrinsic, ft)`.
Assisted-by: Claude Code (Opus 5.5)
- `getfield(x, f, order)` of an atomic field of a bits type of 1 to 8 bytes, or a reference, is an atomic load with the order (with the undefined- reference check), as native code; mismatches between the field and the order stay the runtime's ConcurrencyViolationError. - `Expr(:invoke_modify, ci, modifyfield!, obj, field, op, x, order)` (`@atomic x.f += 1`) of such a field is a loop of the field's atomic compare-and- exchange with `op(old, x)`, calling the CodeInstance of `op` directly, with the write barrier for references, returning `old => new`: native code's loop on Julia 1.12 (from 1.13 on, native code emits `julia.atomicmodify` for a pass to expand). A result of a type that can't be stored, and other fields, call `modifyfield!`. Assisted-by: Claude Code (Opus 5.5)
Calls of type `Union{}` went to the runtime's dynamic dispatch, `throw`
too, which native code (and so GPU compilers, which lower `jl_throw` to
their exception reporting) compiles to `jl_throw`. `throw` now always
reaches `emit_throw`.
Assisted-by: Claude Code (Opus 5.5)
The `_fast` arithmetic and comparison intrinsics, `sqrt_llvm_fast` and the new `min_float_fast`/`max_float_fast` (`llvm.minimum`/`llvm.maximum`) get all fast-math flags, as native code emits them: back ends choose approximate instructions from them (PTX's `div.approx`, `rsqrt.approx`, SPIR-V's `fmin`). Assisted-by: Claude Code (Opus 5.5)
`true`, `false`, `nothing`, `()` and `Core.svec()` are referred to through the runtime's globals (`jl_true`, `jl_false`, `jl_nothing`, `jl_emptytuple`, `jl_emptysvec`), external constants as native code declares them (`global_jlvalue_to_llvm`), rather than literal globals of the module: GPUCompiler relocates them by name, and validates them as libjulia globals. Also: the memory test's allocation count is the least of ten measurements (`@allocated` counts what other threads allocate meanwhile; three still flaked in the full suite). Assisted-by: Claude Code (Opus 5.5)
A value converted to an unboxed union keeps its box in the union's box slot (with the box marker): the object of a constant, as native code keeps isbits constants in unions boxed (JuliaLang/julia#55045), or the box an inline value came from (when it isn't null). Boxing the union then reuses it instead of allocating, as native code; GPUCompiler relocates such boxes as interior records. Assisted-by: Claude Code (Opus 5.5)
…alues
- An llvmcall of a name that isn't an intrinsic LLVM knows, or an `extern`
function, is an error at run time (`jl_error("llvmcall only supports
intrinsic calls")`), as native code compiles it, rather than a declaration
(or Unsupported): GPUCompiler reports it as an unknown intrinsic.
- `pointerset` of a value of another type than the element type checks it
(`emit_typecheck!`, as native `emit_typecheck`: `jl_type_error`) and
unboxes it, rather than calling the intrinsic through the runtime.
Assisted-by: Claude Code (Opus 5.5)
…tive code A C symbol without a library is the runtime's only if libjulia-internal has its `ijl_` alias, as native code decides. Otherwise it's bound lazily on first use from the library `jl_dlfind` finds: - the runtime's own libraries are named by `jl_dlfind`'s sentinels (`JL_EXE_LIBNAME` etc.), which were taken for strings, and are passed with their handle's global (`jl_libjulia_internal_handle`, ...), as native code passes them; - the slot is named as native code's lazily bound functions (`jlplt_name_N_got`), which GPUCompiler reports as such. The check of the library's own symbols called `jl_dlsym` with a `search_deps` argument that Julia 1.12's doesn't have: there it found C library functions (`time`) through the runtime's dependencies. Assisted-by: Claude Code (Opus 5.5)
…he binding - A global that isn't a plain global of the world of the code (imported, not yet resolved, undefined) is read with `jl_get_binding_value_seqcst` on its binding, with `UndefVarError` when it is null, as native code reads it (GPUCompiler reports it as a delayed binding), rather than through a call of `getglobal`. - An assignment of a global with a declared type of a value of that type goes through the binding (release store, write barrier), as native code; values that need converting still go through `setglobal!`. Assisted-by: Claude Code (Opus 5.5)
A varargs method instance's function takes its varargs in a tuple, as its IR has them (`function_argtypes`): - an `:invoke` of a specialized one packs the trailing arguments, as native code calls it; - jlcall entries unpack them from the arguments array: by the signature, or, for a signature ending in `Vararg`, with the runtime's `jl_f_tuple`; - stubs pass them one by one to native code again (splatting a tuple of unknown length). Varargs methods were stubs that called native code. Ones whose signature ends in `Vararg` (as Julia's inference keeps many varargs calls) are still called through `jl_invoke`, with the arguments one by one. The JIT driver collects varargs callees too, and takes the CodeInstances that `:invoke`s refer to as they are: inferring their method instance again can give a CodeInstance of another, normalized, one (`Core.TypeEgal`). Assisted-by: Claude Code (Opus 5.5)
…entries - The column of a statement's innermost location is the statement's index in the IR, as native code encodes it from Julia 1.14 on: GPUCompiler maps errors in the LLVM IR back to the Julia statement they come from by it. - `emit_into!` names the subprogram of a function by its specsig entry (`cgm.debug_linkage`), by which tools look up the function a location is in. Assisted-by: Claude Code (Opus 5.5)
…n, as native code Assisted-by: Claude Code (Opus 5.5)
The specsig entry of a varargs method instance whose signature doesn't end in `Vararg` takes its arguments one by one, as native code declares it, and packs the varargs for the function. Signatures ending in `Vararg` are called in the jlcall convention, as native code does. GPU kernels with varargs (`kernel(args...)`) get their entry. Assisted-by: Claude Code (Opus 5.5)
With a specsig entry, the jlcall entry unboxes its arguments and calls it, instead of inlining the function, as native code's jfptr_f_N calls julia_f_N: GPUCompiler's slow ABI (entry_abi=:func) looks for that call. Assisted-by: Claude Code (Opus 5.5)
As native code's emit_getfield_unknownidx: of the elements of an inline tuple of a single type (extracted from a vector, selected among a few, or loaded from memory: a constant from a private global, which GPU back ends place in constant memory), and of objects whose fields are all references or all of a single type, with the bounds check of the boundscheck argument. These were calls of the runtime's getfield. Assisted-by: Claude Code (Opus 5.5)
Functions take inline aggregates without references by reference to memory that doesn't change (ptr addrspace(11), readonly), as native code's specsig functions, and values keep the memory they were loaded from (a reference argument, an immutable box): calls pass that on rather than copying the value to the stack, and getfield with an index known at run time reads the element where it is. GPUCompiler's back ends then index kernel arguments in place. Assisted-by: Claude Code (Opus 5.5)
nonnull results, and a noalias nocapture noundef readonly argument array, as jfptr_f_N has them (GPUCompiler's slow ABI test checks the declaration). Assisted-by: Claude Code (Opus 5.5)
Assisted-by: Claude Code (Opus 5.5)
Bounds errors call jl_bounds_error_int with the boxed value, or jl_bounds_error_unboxed_int with the data of an inline one without references (as emit_bounds_check), undefined references and division by zero throw the runtime's exception objects through jl_throw (raise_exception_unless), and undefined local variables call jl_undefined_var_error: rather than calls of Julia functions that construct the exceptions, which allocate on GPU back ends, where these throws lower to traps. getfield with an index known at run time also reads constant tuples from memory, as native code, and selects among the references of inline tuples of different types. Assisted-by: Claude Code (Opus 5.5)
svec allocates the vector inline (as native code from Julia 1.12 on), with the elements boxed first, stored with release ordering and write barriers; _svec_len loads the length. These were calls of the runtime, which GPU back ends reject as dynamic calls rather than as unsupported allocations. Assisted-by: Claude Code (Opus 5.5)
debug_info=:no_debug emits the locations in a compile unit of the emission kind NoDebug, as native code for debug_info_kind NoDebug unless Julia runs with -g0: GPUCompiler's error messages read backtraces from them for targets without debug information, then strip them. Assisted-by: Claude Code (Opus 5.5)
As native code, which lays out structs as LLVM structs only for ccall with the llvmcall convention (julia_struct_to_llvm for llvmcall); the conversion also took the length of an array type with length (GPUCompiler's kernel state, a struct of one Int). Assisted-by: Claude Code (Opus 5.5)
…l_typeof The globals that hold the addresses of objects are named as native code's (julia_pgv): +Mod.T# for types, -Mod.f# for methods and method instances, jl_sym#s# for symbols and jl_global# for others, numbered, and carry the julia.constgv metadata; types with a small tag are read from their entry in the runtime's jl_small_typeof (literal_pointer_val_slot). GPUCompiler derives its relocation names from these, and resolves the small type table. Assisted-by: Claude Code (Opus 5.5)
A union whose members carry data is returned as native code's union results: the box, or for a member without references the type index and its data in the buffer union_bytes_return (with UNION_BOX_MARKER for a box); a union of singletons as its type index; others boxed. Both the specsig entries and calls of specsig functions (native code's, from cfunction and jlcall entries) use it. From Julia 1.14 on, native code also returns members with references unboxed, with their roots (JuliaLang/julia#55045): such unions are Unsupported, called in the jlcall convention. Assisted-by: Claude Code (Opus 5.5)
As native code, a union value that has a box (a constant, which unions keep boxed, JuliaLang/julia#55045) is returned as that box, with UNION_BOX_MARKER and its type index, rather than unboxed: jlcall entries returned new boxes of such constants. Assisted-by: Claude Code (Opus 5.5)
…14 on From Julia 1.14 on (JuliaLang/julia#55045), native code's union results also return members with references unboxed: their bytes but the references in the `union_bytes_return` buffer, their references (in the order of their offsets) in a `return_roots` argument with the `julia.return_roots` attribute, which GC lowering roots in the caller's null-initialized stack slots. A member that is only references is returned in the roots alone, with a buffer of their size. The type indices are those of all inline members. Keyed on the dialect's `gc_return_roots === :attribute`, as JuliaIR.jl's GC lowering reads that attribute. Such unions were `Unsupported` on Julia 1.14 and later. Assisted-by: Claude Code (Opus 5.5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #24. These fixes come from running GPUCompiler's test suite with
codegen=:juliacodegen(JuliaGPU/GPUCompiler.jl#1036). Each commit makes JuliaCodegen.jl emit what native code emits where GPUCompiler (or its tests) depends on it.On Julia 1.12, the whole GPUCompiler suite went from 149 failures and 15 errors to 10 failures and no errors (4446 of 4460 pass; Julia's own code generator: 4456). The 10 that remain are IR-shape tests, listed at the end.
Conventions
Varargis still called in the jlcall convention.union_bytes_returnbuffer. A union of singletons comes back as just the index. Others come back boxed. A value that has a box (a constant) is returned as that box, withUNION_BOX_MARKER. This holds for our specsig entries and for our calls of specsig functions (cfunction, jlcall entries). From Julia 1.14 on (implementsret_unionABI for pointer-ful types JuliaLang/julia#55045), members with references are also returned unboxed: their bytes go in the buffer, and their references go in areturn_rootsargument markedjulia.return_roots, which GC lowering roots in the caller's stack slots. This is keyed on JuliaIR'sdialect.gc_return_roots === :attribute.ptr addrspace(11), readonly), as native code's specsig functions do. Values keep the memory they were loaded from (a reference argument, an immutable box). Calls pass that memory on instead of copying to the stack, and dynamic indices read in place.jfptr_f_Ncallsjulia_f_N, instead of inlining the function. They have native code's attributes: anonnullresult and anoalias nocapture noundef readonlyargument array.@noinlinemethods: their functions arenoinline.Builtins and intrinsics
getfieldwith an index known only at run time (emit_getfield_unknownidx), on:Constants are read from a private global, which GPU back ends place in constant memory.
Core.svec, allocated inline (as native code from 1.12 on), andCore._svec_len.Atomic fields: loads with an order, and modifications as a cmpxchg loop.
Fast-math flags on the
_fastintrinsics.throwgoes throughjl_throweven where inference knows the call throws.Error paths through the runtime
These replace calls of Julia functions that construct the exceptions, which allocate on GPU back ends, where these throws lower to traps:
jl_bounds_error_intwith the boxed value, orjl_bounds_error_unboxed_intwith the data of an inline value without references;jl_throw;jl_undefined_var_error;llvmcallof an unknown name:jl_error;pointersetvalues:jl_type_error.llvmcallandccallBase.llvmcallpassesPtrvalues as integers of their size, and structs of fields of one type as arrays, as native code does. Onlyccall(…, llvmcall, …)passes LLVM structs.externfunctions, and overloads of intrinsics by their base name.ijl_aliases. Other symbols are bound lazily, as native code does:jlplt_<sym>_N_gotslots, andjl_dlfindfor the runtime's libraries.Objects and globals
jl_true,jl_nothing, …) through the runtime's globals.jl_small_typeof.+Mod.T#N,-Mod.f#N,jl_sym#s#N,jl_global#N) and carryjulia.constgv. GPUCompiler derives relocation names from these.Debug information
debug_info=:no_debugemits locations in a compile unit of emission kindNoDebug, as native code does fordebug_info_kindNoDebug(unless Julia runs with-g0). GPUCompiler's backtraces need these on PTX.GPUCompiler tests that still fail (IR shape)
nested insertvalue(SPIR-V, 4): native code's nested-indexinsertvalues come from SROA of aggregates built in stack memory. JuliaCodegen.jl builds them in SSA, and LLVM's C API can't emit nested indices.replace i128 allocas(SPIR-V, 4): native code returns the aggregate through sret into a stack slot. JuliaCodegen.jl's internal convention returns it by value, so there is no alloca to rewrite.merged relocation slots(Metal): native code loads an object's address where it is used, and LLVM merges those loads into one from a phi of slots. JuliaCodegen.jl loads each once, at the top of the function.smalltag boxed union constant demotion(Metal): internal calls carry the union's payload, so the kernel never reads the constant's box, and nothing needs to be demoted to an alloca.Tests
Every fix has tests (differential runs, and FileCheck where the shape is the point). Tested locally with
julia +1.12,+1.13,+1.14-nightlyand+nightly: 6114, 6120, 6120 and 6120 tests pass. On nightly, one allocation-count check inmemory.jlfailed only while two suites ran at once, and passes on its own.🤖 Generated with Claude Code