Skip to content

[Cache] Assemble CUDA kernels from per-task modules via a composite JITModule - #875

Merged
hughperkins merged 11 commits into
mainfrom
hp/po-1b-culink-cubin
Aug 20, 2026
Merged

[Cache] Assemble CUDA kernels from per-task modules via a composite JITModule#875
hughperkins merged 11 commits into
mainfrom
hp/po-1b-culink-cubin

Conversation

@hughperkins

@hughperkins hughperkins commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Split CUDA kernel compilation per offloaded task, but keep it toolkit-free. Each offloaded task becomes its own self-contained module (PerConstructArtifact{ module }); at JIT time each is compiled to PTX and loaded as its own CUmodule via cuModuleLoadDataEx. JITModuleCUDA becomes a composite that holds the N modules and resolves task entry points by name across them (memoised). The launcher and CUDA-graph builder are unchanged -- they only ever look tasks up by name.

This is "Option B" of the per-task compile design: it deliberately avoids ptxas/cuLink/a relocatable-cubin cache, so the path needs no CUDA toolkit at runtime and adds nothing to the wheel. (An earlier revision of this branch used ptxas -c + cuLink + a disk cubin cache; that requires a PTX assembler, which driver-only wheels and the GPU CI runners do not have -- hence the previous push's ptxas -c ... rc=32512 failures.)

What per-task buys us

Per-task reuse rides on the pre-existing cache tiers, now at task granularity because each task is a separate module:

  • PtxCache (PTX, keyed on LLVM-IR text) -- an unchanged task skips LLVM->PTX.
  • the NVIDIA driver compute cache (~/.nv/ComputeCache, SASS, keyed on PTX hash) -- an unchanged task skips ptxas inside the driver.

No new on-disk cache is introduced here; the cross-process per-task artifact cache is ref 6. 1b is the enabler: it makes each task an independently-compiled, independently-loadable unit that 6 / 8 / 10 build on.

Changes

  • runtime/cuda/jit_cuda.{h,cpp}: composite JITModuleCUDA (vector<CUmodule> + name->CUfunction memo); add_module_per_task compiles each task module to PTX and driver-loads it; deletes the ptxas/cuLink/cubin-cache code.
  • jit/jit_session.h, runtime/llvm/llvm_runtime_executor.{h,cpp}, runtime/cuda/kernel_launcher.cpp: rename add_module_culink / create_jit_module_culink -> ..._per_task.
  • codegen/codegen.cpp, codegen/llvm/{codegen_llvm.cpp,llvm_compiled_data.h}: build one self-contained module per task into per_construct_artifacts (PerConstructArtifact is { module }; the full record moves to 6).
  • codegen/cuda/codegen_cuda.cpp: emit static shared scratch and bls_buffer as internal definitions so each per-task module is self-contained.
  • tests/python/test_atomic.py: widen f16 tolerance (per-task codegen changes atomic interleaving order).
  • docs/source/user_guide/init_options.md: trim the offline-cache description.

Testing

RTX PRO 6000 (sm_120), CUDA container, editable build:

  • test_atomic -- 96 passed.
  • reduction graph struct_for offload_cross offline_cache -- 82 passed (exercises BLS / shared-memory reductions, CUDA-graph capture across the composite module, multi-offload struct-for, and offline-cache reuse).
  • pre-commit run on the changed files -- clean.

The whole-module path recompiles every task's PTX whenever any part of the
kernel changes, because the LLVM text it hashes moves even for tasks that did
not. Build one self-contained module per offloaded task instead, assemble each
to a relocatable cubin with `ptxas -c`, and device-link them into a single
CUmodule with cuLink. The cubins are cached on disk keyed by the hash of each
task module's LLVM-IR text, so an unchanged task skips PTX generation and ptxas
entirely on a warm edit.

Self-containment requires `bls_buffer` and static shared scratch to be internal
definitions rather than external declarations: with one cubin per task there is
no whole-module definition left to resolve against, and cuLinkComplete fails on
the undefined reference.

The cubin directory is namespaced by SM version, since `ptxas -arch=` output is
not portable across GPUs. The f16 tolerance in test_atomic_float_ops is widened
because cuLink changes code layout and so the interleaving of 128 concurrent
atomics; f16 cannot represent their exact sum at the old 1e-3 either way.

The cubin cache honors offline_cache=False: like PtxCache and the kernel-data
cache it then falls back to a mem-only build that touches no disk, so a fresh
session with offline_cache=False cannot be served a cubin left by a previous
run. init_options.md documents this new disk tier alongside the existing ones.

compile_module_to_ptx NUL-terminates the PTX buffer for the driver's
cuModuleLoadDataEx, but ptxas (CUDA 13+) treats the embedded NUL as a premature
EOF and aborts; write only up to the terminator when handing PTX to ptxas.
1b's assemble path consumes only `module` (the relocatable-cubin cache keys on
the module's LLVM text); the record's other fields -- key, cubin, tasks,
used_tree_ids, struct_for_tls_sizes -- are populated in codegen.cpp but read
nowhere in 1b (the launch path uses the kernel-level LLVMCompiledKernel::tasks).
Per the no-scaffolding rule they are 6's, so keep PerConstructArtifact at just
{ module } and let 6 reintroduce the full record with the artifact cache that is
their sole producer and consumer. The per_construct_artifacts vector stays -- it
carries the modules to the JIT.
Condense the explanatory comments added by the relocatable-cubin path to
the essential intent; no code changes.
Drop the per-layer cache breakdown from the user-facing offline_cache
option; keep just the cache folder and the cold-start behavior.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4772e5ef6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread quadrants/runtime/cuda/jit_cuda.cpp Outdated
Comment on lines +202 to +204
auto cmd = fmt::format("ptxas -c -arch={} {} -o {} 2>/dev/null", arch, ptx_path.string(), cubin_path.string());
int rc = std::system(cmd.c_str());
QD_ERROR_IF(rc != 0, "ptxas -c failed (rc={}) arch={}", rc, arch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid an unconditional POSIX ptxas subprocess

On a fresh cache, every newly compiled CUDA kernel now reaches this command because the launcher selects the nonempty per-task artifacts, but supported driver/runtime-only installations do not necessarily provide the toolkit's ptxas executable (QD_WITH_CUDA_TOOLKIT is off by default). Such installations now fail with ptxas -c failed instead of using the driver's existing PTX JIT; moreover, the /dev/null redirection and unquoted temporary paths make this command unsuitable for the supported Windows CUDA configuration even when ptxas is installed. Please use the driver APIs or retain the whole-module fallback when this path is unavailable, and keep the installation requirements synchronized if a toolkit is intentionally required.

AGENTS.md reference: AGENTS.md:L15-L22

Useful? React with 👍 / 👎.

Comment on lines +607 to +609
if (!data.per_construct_artifacts.empty()) {
// Per-task cubin path: cuLink the per-task modules instead of loading the whole-module PTX.
jit_module = executor->create_jit_module_culink(std::move(data.per_construct_artifacts));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the existing PTX debugging controls

For every freshly compiled CUDA kernel, this branch bypasses JITSessionCUDA::add_module, which is the only path that implements print_kernel_asm and QUADRANTS_LOAD_PTX. Consequently, print_kernel_asm=True no longer writes the documented PTX file, and QUADRANTS_LOAD_PTX=1 silently executes newly generated PTX rather than the user's edited dump, despite the documented workflow requiring both caches to be disabled. Please handle these controls on the cuLink path or select the legacy path when either is active.

AGENTS.md reference: AGENTS.md:L15-L22

Useful? React with 👍 / 👎.

Comment thread quadrants/codegen/codegen.cpp Outdated
Comment on lines +81 to +88
std::vector<PerConstructArtifact> per_construct_artifacts;
for (int i = 0; i < (int)data.size(); i++) {
if (!data[i] || !data[i]->module)
continue;
PerConstructArtifact art;
std::vector<std::unique_ptr<LLVMCompiledTask>> one;
one.push_back(std::make_unique<LLVMCompiledTask>(data[i]->clone()));
auto linked_one = tlctx_.link_compiled_tasks(std::move(one));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate per-task artifact generation to CUDA

This common LLVM compilation path now clones, links, and optimizes a self-contained module for every task on CPU and AMDGPU as well as CUDA. The CPU and AMDGPU launchers never consume per_construct_artifacts, so kernels on those backends pay up to one extra runtime-module link and optimization pipeline per task before the original whole-module link still runs. Restrict this CUDA-only feature to the CUDA codegen path to avoid a potentially large compile-time and memory regression on unrelated backends.

AGENTS.md reference: AGENTS.md:L5-L13

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Apply clang-format reflows and reword one comment so it fills to 120
cols rather than breaking early before a long path token.
Comment thread quadrants/runtime/cuda/jit_cuda.cpp Outdated
Comment on lines +243 to +245
if (!dump_ir && fs::exists(cubin_path)) {
std::ifstream in(cubin_path, std::ios::binary);
return std::vector<char>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

File handle error not checked: if the file exists but cannot be opened (permissions, race condition, etc.), an empty vector will be returned, causing cuLink to fail later with cryptic errors.

if (!dump_ir && fs::exists(cubin_path)) {
  std::ifstream in(cubin_path, std::ios::binary);
  if (!in.is_open()) {
    QD_ERROR("Failed to open cached cubin: {}", cubin_path);
  }
  return std::vector<char>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
Suggested change
if (!dump_ir && fs::exists(cubin_path)) {
std::ifstream in(cubin_path, std::ios::binary);
return std::vector<char>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!dump_ir && fs::exists(cubin_path)) {
std::ifstream in(cubin_path, std::ios::binary);
if (!in.is_open()) {
QD_ERROR("Failed to open cached cubin: {}", cubin_path);
return std::vector<char>();
}
return std::vector<char>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

Option B: compile each self-contained task module to PTX and load it as
its own CUmodule via cuModuleLoadDataEx; JITModuleCUDA is now a composite
that resolves task entry points by name across the N modules (memoised).
This removes the ptxas subprocess, the relocatable-cubin disk cache, and
the cuLink device link, so the per-task path runs on a driver-only machine
with no CUDA toolkit and no wheel-size growth. Per-task reuse rides on the
existing PtxCache (PTX) plus the NVIDIA driver compute cache (SASS).

Rename add_module_culink -> add_module_per_task and create_jit_module_culink
-> create_jit_module_per_task, and refresh the now-inaccurate cuLink/relocatable
comments on the shared-array linkage fix and the test_atomic tolerance.
@hughperkins hughperkins changed the title [Cache] Assemble CUDA kernels from per-task relocatable cubins [Cache] Assemble CUDA kernels from per-task modules via a composite JITModule Aug 19, 2026
@github-actions

Copy link
Copy Markdown

The composite per-task path dropped the QD_DUMP_IR / QD_LOAD_IR side effects
that the old cuLink path emitted, so test_loop_config_name_cuda_ptx_dump found
no .ptx under debug_dump_path. Factor add_module's dump/load block into a shared
compile_module_to_ptx_with_dump() and call it from add_module_per_task too, so
both paths dump <name>_before_ptx.ll / <name>.ptx (and honour QD_LOAD_IR)
identically.
@github-actions

Copy link
Copy Markdown

@hughperkins

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70e9c9a64d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docs/source/user_guide/init_options.md Outdated
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

compile_kernel_to_module built a self-contained per-task module for every
task on all LLVM backends, but only the CUDA launcher consumes
per_construct_artifacts. That made CPU / AMDGPU pay an extra per-task
link + optimize for artifacts they never use. Gate the loop on Arch::cuda
(refs 7/8 extend consumption to CPU/AMD).
offline_cache=False disables Quadrants' own on-disk caches, but the NVIDIA
driver keeps a separate SASS cache at ~/.nv/ComputeCache that the flag does
not disable (the per-process nonce only stops cross-run reuse). Correct the
"nothing is read from or written to disk" claim, which misled users profiling
disk activity or on read-only filesystems.
@github-actions

Copy link
Copy Markdown

@hughperkins

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 278639b8d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

#include "quadrants/runtime/llvm/llvm_fwd.h"
#include "quadrants/util/lang_util.h"
#include "quadrants/jit/jit_module.h"
#include "quadrants/codegen/llvm/llvm_compiled_data.h" // PerConstructArtifact (per-task module path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the generic JIT header independent of LLVM artifacts

When configuring a supported QD_WITH_LLVM=OFF build, QuadrantsCore.cmake still compiles quadrants/jit/jit_session.cpp, so this unconditional include now reaches llvm/IR/Module.h even though LLVM discovery and include directories are only configured when LLVM is enabled. Vulkan/Metal-only builds on systems without LLVM headers therefore fail before the existing #ifdef QD_WITH_LLVM in jit_session.cpp; gate or forward-declare the CUDA-only artifact API rather than importing llvm_compiled_data.h into the generic JIT header.

AGENTS.md reference: AGENTS.md:L9-L11

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

@hughperkins

Copy link
Copy Markdown
Collaborator Author

I think I'm going to ignore hte init_opt.md faeilures on this PR. They're being handled seprately, in #873

jit_session.cpp is globbed into the core lib unconditionally, so the generic
JIT header must not hard-depend on LLVM headers. The per-task include of
llvm_compiled_data.h pulled in llvm/IR/Module.h, breaking Vulkan/Metal-only
(QD_WITH_LLVM=OFF) builds where LLVM include dirs are not configured. Gate the
include and the add_module_per_task declaration behind QD_WITH_LLVM, matching
how jit_session.cpp already guards its LLVM guts; the rest of the header keeps
using forward-declared llvm types via llvm_fwd.h.
@hughperkins

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 82ba9efb86

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@hughperkins

Copy link
Copy Markdown
Collaborator Author

running Genesis benchmarks

@hughperkins

Copy link
Copy Markdown
Collaborator Author

running Genesis unit tests

@hughperkins

Copy link
Copy Markdown
Collaborator Author

benchmarks at this point:

20260820_1b_0827

I think I need to run the before/after script, on a single node.

@hughperkins

Copy link
Copy Markdown
Collaborator Author

(running before/after script on single node for convexify and table bussing)

@github-actions

Copy link
Copy Markdown

@hughperkins

Copy link
Copy Markdown
Collaborator Author

results of before/after, same node, 5 runs of each:

=== before/after interleaved (5 runs each, single node) ===
before qd=96c594499  after qd=a114f1227  genesis=d00c93b3

env                     batch  back   gjk    solv   n     before_fps     ±%      after_fps     ±%   delta%  min_sig%
convexify                   0   cpu                 4           54.8   9.89           57.2   4.17     4.57      9.43
table_bussing              50   cpu                 4          138.5  10.88          150.0   9.29     8.30     11.97

@hughperkins

Copy link
Copy Markdown
Collaborator Author

Genesis unit tests ok:

Screenshot 2026-08-20 at 09 48 37

@hughperkins

Copy link
Copy Markdown
Collaborator Author

I think I will wait for CI to pass-ish, then merge

@github-actions

Copy link
Copy Markdown

@hughperkins
hughperkins merged commit bdb9b49 into main Aug 20, 2026
66 of 67 checks passed
@hughperkins
hughperkins deleted the hp/po-1b-culink-cubin branch August 20, 2026 14:45
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