Skip to content

[C++][XLA Binding] executable serialization + allocator/compiled stats - #3277

Merged
wsmoses merged 1 commit into
EnzymeAD:mainfrom
csvance:feature/executable-serialization-memory-stats
Sep 21, 2026
Merged

wsmoses merged 1 commit into
EnzymeAD:mainfrom
csvance:feature/executable-serialization-memory-stats

Conversation

@csvance

@csvance csvance commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

This PR exposes executable serialization for PJRT so programs can be re-used without needing any kind of compile. This matters for ReactantServer.jl because every time the server restarts it can mean compiling 100+ mlir bundles, and even with an auto-tune cache this can take 30+ minutes.

It also exposes more detailed allocation usage and allows for clearing the memory high water mark for applications which need to measure exact memory water marks such as ReactantServer.

Without these provided by Reactant, I have to maintain my own downstream PJRT C bindings or patched Reactant jll which obviously isn't ideal in the long run. I also included IFRT bindings for the same functionality rather than just raising some sort of not implemented exception.

Working on building and testing this on my end. I confirmed that the general approach I took here works inside of ReactantServer via my vendored PJRT interface but it's one thing to just do all of the PJRT calls on my end and actually get this successfully upstreamed. Currently building Reactant jll locally so I can do a full end-to-end test of the changes.

@csvance

csvance commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

I found one problem in the IFRT bindings with my local build, will push a fix shortly. I'm currently testing these changes inside ReactantServer with a huge model repository.

@csvance
csvance force-pushed the feature/executable-serialization-memory-stats branch from 2447a46 to 06c35e0 Compare September 12, 2026 20:17
@csvance

csvance commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

I have a ReactantServer feature branch which uses the the API changes here successfully, Server startup time is down from nearly 30 minutes to just under 3: EnzymeAD/ReactantServer.jl@main...feature/reactant-executable-cache

Not sure about CI / whether I can get the tests to pass before the new Reactant jll is built. I have my own internal Bazel build pulling the patched Reactant jll version here: https://github.com/csvance/Reactant.jl/releases/tag/reactantextra-local-06c35e0d

@csvance
csvance marked this pull request as ready for review September 12, 2026 20:44
@csvance
csvance requested a review from wsmoses September 16, 2026 01:14
compile_options_proto_size));
}

#pragma region PjRtLoadedExecutable serialization and memory stats

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.

what is pragma region

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@wsmoses it looks like it was used many times elsewhere in API.cpp, probably because the file is so large and it serves as a way to collapse the block in an IDE? I can remove it if it would make the changes sit cleaner in the file or try and group things differently.

@csvance

csvance commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

@wsmoses the tests I add pass with the local libReactant / Build Reactant_jll and I'm actively using the changes as part of a test deployment, everything is working as expected. There are some pre-existing test failures that show up in two of the local libReactant test builds, but those were there before my changes.

Merging this in would open up the potential to have a better kernel cache in Reactant beyond just autotuning. It's possible that I could eventually move ReactantServer off of the internal jll PJRT API and onto the public Reactant interface. These changes would be the first step towards that.

@Pangoraw

Copy link
Copy Markdown
Collaborator

Curious if you tried with using the compiled artifact with multi-gpu ? There was some problems last time i tried in #2779.

@csvance

csvance commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Curious if you tried with using the compiled artifact with multi-gpu ? There was some problems last time i tried in #2779.

I will test it out shortly; should be able to rule out whether there is some sort of issue on the XLA side since that's the only thing my PR touches.

@csvance

csvance commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

@Pangoraw I tried a few different variations with both PJRT and IFRT on multiple GPUs (nVidia A6000):

  • Serialize on GPU 0,1, load on GPU 0,1
  • Serialize on GPU 0,1, load on GPU 1,0 (change order w/ CUDA_VISIBLE_DEVICES)

I don't currently have a GPU free to test something like 0,1 -> 1,2 or 0,1 -> 2,3.

The output of the compiled program was the same no matter which order it was loaded on. I only tried on a single machine; I don't have a cluster where I could try something distributed.

@csvance

csvance commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

@wsmoses should I go ahead and merge this? Unsure of the process since it involves a bump to the jll.

@Pangoraw

Copy link
Copy Markdown
Collaborator

should I go ahead and merge this? Unsure of the process since it involves a bump to the jll.

to trigger a new jll build, you need to make a PR to https://github.com/EnzymeAD/ReactantBuilder targeting the relevant commit on Reactant main and bumping the Reactant_jll version.

Comment thread src/xla/Utils.jl Outdated
Comment on lines +59 to +60
bytes = len == 0 ? UInt8[] : copy(unsafe_wrap(Array, ptr, (Int(len),); own=false))
@ccall free(ptr::Ptr{UInt8})::Cvoid

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not ?

Suggested change
bytes = len == 0 ? UInt8[] : copy(unsafe_wrap(Array, ptr, (Int(len),); own=false))
@ccall free(ptr::Ptr{UInt8})::Cvoid
return unsafe_wrap(Array, ptr, (Int(len),); own=true)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@Pangoraw I'll extract it from the helper in Utils.jl and just apply the unsafe_wrap(...; own=true) pattern to the two call sites. unsafe_wrap already correctly handles len == 0 here, better to just simplify.

@wsmoses wsmoses left a comment

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.

also the api.cpp changes and the julia changes should be separate prs

@csvance
csvance force-pushed the feature/executable-serialization-memory-stats branch from 0d63a6a to 3238dd5 Compare September 21, 2026 13:50
@csvance csvance changed the title [XLA Binding] executable serialization + allocator/compiled stats [C++][XLA Binding] executable serialization + allocator/compiled stats Sep 21, 2026
@csvance

csvance commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator Author

@wsmoses I made this PR the C++ side only and extracted the Julia side into #3305

The only changes were running clang tidy so the code-style-cpp check passes.

…mpiled memory stats

XLA can serialize a compiled executable and load it again later, reset a device
allocator's peak counters, and report the compiler's static memory accounting for an
executable, but none of that was reachable through libReactantExtra. This adds the C++
side only; the Julia bindings, API and tests follow in a separate PR once a Reactant_jll
has been built from this.

- PjRtLoadedExecutableSerialize / PjRtClientLoadSerializedExecutable (optional
  CompileOptionsProto override so a program can be placed on another device)
- PjRtDeviceClearMemoryStats
- PjRtLoadedExecutableGetCompiledMemoryStats with a flat JLCompiledMemoryStats struct
- IFRT counterparts for the PJRT-backed IFRT client, going through IFRT's own
  LoadedExecutable::Serialize and Compiler::DeserializeLoadedExecutable
- GetCpuPjrtApi, linking @xla//xla/pjrt/c:pjrt_c_api_cpu_internal, so the CPU plugin's
  PJRT C API table is reachable next to the GPU one
- Darwin exported-symbol entries for the new names

Built locally with deps/build_local.jl (CUDA) and exercised through the Julia bindings
on CPU, on two RTX A6000s, and under the IFRT runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187bPmjLReauAiN6YP2a3cd
@wsmoses
wsmoses force-pushed the feature/executable-serialization-memory-stats branch from 3238dd5 to 940d651 Compare September 21, 2026 17:00
@wsmoses
wsmoses merged commit d420ec7 into EnzymeAD:main Sep 21, 2026
3 of 8 checks passed
csvance added a commit to csvance/Reactant.jl that referenced this pull request Sep 22, 2026
…ory stats (Julia)

Julia side of the shims added to libReactantExtra in EnzymeAD#3277: XLA.serialize_executable,
XLA.load_serialized_executable, XLA.clear_memory_stats!, XLA.compiled_memory_stats and
the CompiledMemoryStats struct, with PJRT and IFRT methods and Thunk conveniences for
serialize and compiled stats. The serialized bytes take ownership of the malloc'd buffer
through unsafe_wrap(...; own=true) instead of being copied.

test/core/executable_serialization.jl: round trip with bit-identical results,
compile-options override, invalid bytes, memory stats, stats reset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187bPmjLReauAiN6YP2a3cd
csvance added a commit to csvance/Reactant.jl that referenced this pull request Sep 22, 2026
…ory stats (Julia)

Julia side of the shims added to libReactantExtra in EnzymeAD#3277: XLA.serialize_executable,
XLA.load_serialized_executable, XLA.clear_memory_stats!, XLA.compiled_memory_stats and
the CompiledMemoryStats struct, with PJRT and IFRT methods and Thunk conveniences for
serialize and compiled stats. The serialized bytes take ownership of the malloc'd buffer
through unsafe_wrap(...; own=true) instead of being copied.

test/core/executable_serialization.jl: round trip with bit-identical results,
compile-options override, invalid bytes, memory stats, stats reset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187bPmjLReauAiN6YP2a3cd
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