Skip to content

Add runtime namespace renaming for import/export/internal scopes - #9240

Open
derek-gerstmann wants to merge 16 commits into
mainfrom
dg/runtime_namespace
Open

Add runtime namespace renaming for import/export/internal scopes#9240
derek-gerstmann wants to merge 16 commits into
mainfrom
dg/runtime_namespace

Conversation

@derek-gerstmann

@derek-gerstmann derek-gerstmann commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Implement runtime namespace symbol renaming for import/export/internal scopes.

Use Case:

Enable custom runtimes to be built that partially override existing behavior,
by encapsulating the Halide runtime methods into namespaced functions
with user given prefixes for three scopes:

  • The "import" prefix corresponds to the method names called from within a generated kernel
  • The "export" prefix corresponds to the method names externally visible in the runtime library
  • The "internal" prefix corresponds to the method names called within the runtime library

Implementation:

Rename halide runtime symbols in generated modules according to user-supplied
prefixes, so runtimes and kernels can be built with a custom ABI namespace that
does not collide with other Halide runtimes -- including keeping each runtime's
mutable state independent when several differently-namespaced runtimes are
linked into the same process.

LLVM backend (apply_runtime_namespace_prefixes in CodeGen_LLVM, a single pass
over the final llvm::Module; call sites and initializers auto-follow via
use-lists):

  • The halide_-prefixed extern "C" ABI, replacing the leading "halide_":

    • definition -> export prefix
    • declaration called by the kernel -> import prefix
    • declaration called only by other runtime methods -> internal prefix
      Import vs. internal is decided by the caller (kernel entry points vs. other
      runtime methods); kernel-import wins for a declaration with mixed callers.
  • The runtime's internal C++ symbols in the Halide::Runtime::Internal namespace
    (functions and, crucially, the linkonce state globals such as custom_malloc,
    the thread-pool work queue, the memoization cache, ...). These carry no
    "halide_" to replace, so the internal prefix is prepended. Without this, two
    namespaced runtimes' linkonce state globals would be merged into one shared
    copy, defeating isolation of the renamed halide_set_/halide_get_ methods.

C backend (CodeGen_C): a kernel that calls an external runtime, so only the
import prefix applies. It emits a block of #define halide_x <prefix>x for the
runtime C ABI functions (discovered by scanning the embedded runtime header) at
the top of the generated source (never the header, so namespaced headers can
be co-included). The preprocessor rewrites the runtime declarations and all call
sites consistently while leaving types, typedefs, and enum values untouched.

Pipeline entry points and libc symbols are left untouched. JIT is rejected: it
resolves runtime calls against a process-global, non-namespaced shared runtime.

Details:

  • Module::set_runtime_namespace_map; Pipeline::apply_runtime_namespace stores
    the params (and rejects JIT); compile_to_module attaches the map to the Module
    and guards JIT; AbstractGenerator wires the GeneratorContext prefixes through;
    Module.cpp forwards the import prefix to CodeGen_C for c_source output.
  • The generator command-line -r (standalone runtime) path forwards
    runtime_namespace.{import,export,internal} params to compile_standalone_runtime.
  • add_halide_runtime() (CMake) gains a PARAMS argument, forwarded to GenRT, so
    namespaced runtimes can be produced from the build system.

Docs: doc/CustomRuntimes.md describes the use case, scopes, backends, and usage
from C++, the GenGen command line, and CMake.

Tests:

  • test/correctness/runtime_namespace.cpp: baseline, export, import, internal
    (namespace-state renaming + the import/internal discriminator), all
    optional-prefix combinations for AOT and NoRuntime, and the JIT error case.
  • test/generator/runtime_namespace_iso: builds three variants of one pipeline
    with both the LLVM and the C backend, each linked against a runtime with a
    distinct namespace (none/a/b), links them all into one program, and checks
    that per-runtime state stays independent across the custom allocator,
    halide_set/get_num_threads, and the error handler, while each pipeline still
    computes correctly.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Issues:

This only covers AOT. For JIT, I'm not sure how to handle this reliably, so we currently
assert if a namespace prefix is specified.

Checklist

  • Tests added or updated (not required for docs, CI config, or typo fixes)
  • Documentation updated (if public API changed)
  • Python bindings updated (if public API changed)
  • Benchmarks are included here if the change is intended to affect performance.
  • Commits include AI attribution where applicable (see Code of Conduct)

Derek Gerstmann and others added 4 commits July 15, 2026 16:58
…l scopes

Rename halide_-prefixed runtime symbols in generated modules according to
user-supplied prefixes, so runtimes and kernels can be built with a custom
ABI prefix that doesn't collide with other Halide runtimes.

- Add apply_runtime_namespace_prefixes in CodeGen_LLVM: a single pass over the
  final llvm::Module (call sites auto-follow renamed functions via use-lists).
  Classifies each halide_ symbol by role:
    * definition                       -> export prefix
    * declaration called by the kernel -> import prefix
    * declaration called only by other runtime methods -> internal prefix
  Only halide_-prefixed symbols are touched; pipeline entry points and libc
  symbols are left alone.
- Plumb RuntimeNamespaceParams from Pipeline/Generator onto the Module:
  Module::set_runtime_namespace_map, Pipeline::apply_runtime_namespace stores
  the params, compile_to_module attaches the map, and AbstractGenerator wires
  the GeneratorContext prefixes through.
- Reject runtime-namespace prefixes on JIT targets: the JIT resolves runtime
  calls against a process-global, non-namespaced shared runtime.
- Fix GeneratorParam_RuntimeNamespaceParams::try_set, whose
  (key == n) && starts_with(key, n + ".") guard could never match, so
  runtime_namespace.import/export/internal were never parsed.
- Add test/correctness/runtime_namespace.cpp covering baseline, export, import,
  the import/internal discriminator, all optional-prefix combinations for AOT
  and NoRuntime, and the JIT error case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@derek-gerstmann
derek-gerstmann requested a review from abadams July 24, 2026 18:18
@abadams

abadams commented Jul 24, 2026

Copy link
Copy Markdown
Member

There are also internal globals not prefixed with halide_ in the runtime - i.e. stuff in Halide::Runtime::Internal. I suggest an aot generator test (or demo app, if that's easier) that links two differently-prefixed runtimes in the same process, and has a pipeline that uses each. It could check that they don't interfere with each other by tweaking internal state of each (e.g. number of threads in the thread pool, the existence of a custom malloc override, etc).

@derek-gerstmann

Copy link
Copy Markdown
Contributor Author

Ah, yes ... good idea. I'll add an AOT generator and binary that links in multiple namespaced runtimes to validate we have no collisions.

…l scopes

Rename halide runtime symbols in generated modules according to user-supplied
prefixes, so runtimes and kernels can be built with a custom ABI namespace that
does not collide with other Halide runtimes -- including keeping each runtime's
mutable state independent when several differently-namespaced runtimes are
linked into the same process.

LLVM backend (apply_runtime_namespace_prefixes in CodeGen_LLVM, a single pass
over the final llvm::Module; call sites and initializers auto-follow via
use-lists):

- The halide_-prefixed extern "C" ABI, replacing the leading "halide_":
    * definition                          -> export prefix
    * declaration called by the kernel    -> import prefix
    * declaration called only by other runtime methods -> internal prefix
  Import vs. internal is decided by the caller (kernel entry points vs. other
  runtime methods); kernel-import wins for a declaration with mixed callers.

- The runtime's internal C++ symbols in the Halide::Runtime::Internal namespace
  (functions and, crucially, the linkonce state globals such as custom_malloc,
  the thread-pool work queue, the memoization cache, ...). These carry no
  "halide_" to replace, so the internal prefix is prepended. Without this, two
  namespaced runtimes' linkonce state globals would be merged into one shared
  copy, defeating isolation of the renamed halide_set_*/halide_get_* methods.

C backend (CodeGen_C): a kernel that calls an external runtime, so only the
import prefix applies. It emits a block of `#define halide_x <prefix>x` for the
runtime C ABI functions (discovered by scanning the embedded runtime header) at
the top of the generated *source* (never the header, so namespaced headers can
be co-included). The preprocessor rewrites the runtime declarations and all call
sites consistently while leaving types, typedefs, and enum values untouched.

Pipeline entry points and libc symbols are left untouched. JIT is rejected: it
resolves runtime calls against a process-global, non-namespaced shared runtime.

Plumbing:
- Module::set_runtime_namespace_map; Pipeline::apply_runtime_namespace stores
  the params (and rejects JIT); compile_to_module attaches the map to the Module
  and guards JIT; AbstractGenerator wires the GeneratorContext prefixes through;
  Module.cpp forwards the import prefix to CodeGen_C for c_source output.
- The generator command-line -r (standalone runtime) path forwards
  runtime_namespace.{import,export,internal} params to compile_standalone_runtime.
- add_halide_runtime() (CMake) gains a PARAMS argument, forwarded to GenRT, so
  namespaced runtimes can be produced from the build system.
- Fixes GeneratorParam_RuntimeNamespaceParams::try_set, whose
  (key == n) && starts_with(key, n + ".") guard could never match.

Docs: doc/CustomRuntimes.md describes the use case, scopes, backends, and usage
from C++, the GenGen command line, and CMake.

Tests:
- test/correctness/runtime_namespace.cpp: baseline, export, import, internal
  (namespace-state renaming + the import/internal discriminator), all
  optional-prefix combinations for AOT and NoRuntime, and the JIT error case.
- test/generator/runtime_namespace_iso: builds three variants of one pipeline
  with both the LLVM and the C backend, each linked against a runtime with a
  distinct namespace (none/a/b), links them all into one program, and checks
  that per-runtime state stays independent across the custom allocator,
  halide_set/get_num_threads, and the error handler, while each pipeline still
  computes correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.73737% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.31%. Comparing base (faa3500) to head (62b3c0c).

Files with missing lines Patch % Lines
src/Generator.cpp 56.89% 18 Missing and 7 partials ⚠️
src/CodeGen_LLVM.cpp 79.74% 9 Missing and 7 partials ⚠️
src/CodeGen_C.cpp 80.76% 1 Missing and 4 partials ⚠️
src/Pipeline.cpp 70.00% 1 Missing and 2 partials ⚠️
src/Generator.h 50.00% 2 Missing ⚠️
src/Pipeline.h 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9240      +/-   ##
==========================================
+ Coverage   70.13%   70.31%   +0.17%     
==========================================
  Files         255      255              
  Lines       78895    79079     +184     
  Branches    18865    18926      +61     
==========================================
+ Hits        55332    55602     +270     
+ Misses      17866    17852      -14     
+ Partials     5697     5625      -72     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Derek Gerstmann and others added 4 commits July 24, 2026 15:58
…e implementation

Resolve conflicts in src/CodeGen_LLVM.cpp and test/correctness/runtime_namespace.cpp
in favor of the local branch, which supersedes the remote's earlier snapshot
(adds the Halide::Runtime::Internal state-global renaming, the C backend, and the
my_ns_ example prefixes).
The prior binding of compile_standalone_runtime() took a
std::map<RuntimeVisibility, std::string> but RuntimeVisibility was never bound,
so the map could not be constructed from Python, and the map argument had no
default (a regression for existing callers).

- Bind the RuntimeVisibility enum (Import/Export/Internal).
- Give compile_standalone_runtime()'s namespace_map argument an empty default.
- Bind Pipeline.apply_runtime_namespace(target, namespace_map), accepting a dict
  keyed by RuntimeVisibility (rejects JIT targets, matching the C++ API).
- Add python_bindings/test/correctness/runtime_namespace.py covering the enum,
  a namespaced standalone runtime, the optional/default map, a namespaced AOT
  pipeline, and the JIT error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Correctness tests (runtime_namespace.cpp) and the Python test are picked up
automatically (the former via the correctness glob; the Makefile does not build
the Python bindings tests). The generator isolation test needs custom rules
because it links three separately namespaced standalone runtimes plus both
LLVM- and C-backend kernels for each into a single binary, which the generic
single-runtime generator rules cannot express.

- Add rules for the three namespaced runtimes (stock, runtime_a_, runtime_b_)
  and the six kernels (LLVM + C backend, distinct function names, matching
  import/internal prefixes), forwarding runtime_namespace.* to the generator.
- Add a custom generator_aot_runtime_namespace_iso link rule that combines them.
- Exclude generator_aotcpp_runtime_namespace_iso: the single test already links
  both backends, so there is no separate C++-backend-only variant.

Note: the Makefile build could not be exercised in this environment (missing
libpng/pkg-config); the rules follow the existing alias/cxx_mangling/pyramid
precedents and the -r -e static_library + runtime_namespace.* flags were
verified against a built generator binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@derek-gerstmann

derek-gerstmann commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author
  • Added global symbol handling
  • Added AOT multi-runtime test case
  • Added C backend equivalents for renaming
  • Added missing python binding updates
  • Added CustomRuntimes.md documentation

@derek-gerstmann

Copy link
Copy Markdown
Contributor Author

@alexreinking Have a look at the added PARAMS in the CMake helper. Let me know if you feel this is an appropriate way of handling additional key=value settings for generators.

@derek-gerstmann

derek-gerstmann commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Currently investigating failures ... it appears there's still some duplicate symbol collisions from global vars. Working on a fix.

@abadams

abadams commented Jul 27, 2026

Copy link
Copy Markdown
Member

Some of the failures may be because we have bad hygiene about how we name things inside the runtime, which should be fixed by renaming them (e.g. adding halide_), rather than making the namespacing pass more complex.

…(GPU crash)

The internal-scope rename only renamed symbols in the Halide::Runtime::Internal
mangled namespace. GPU device runtimes also define externally-linked helpers
that are neither halide_-prefixed nor in that namespace (e.g. is_compiled_metallib,
copy_to_device_already_locked, metal_device_crop_from_offset). These escaped the
rename, so linking several differently-namespaced runtimes into one process
merged them into a single shared copy -> shared device state -> segfaults on GPU
targets.

Identify runtime-internal symbols precisely instead of by name: snapshot the
symbols present in the initial runtime-only module (CodeGen_LLVM::init_module,
before any pipeline functions are emitted), and for the Internal scope rename
every *defined* symbol from that set except the halide_ C ABI (handled by the
export/import scopes) and LLVM's reserved globals. Declarations (the runtime's
libc imports such as strstr/write/usleep) are left untouched so they still bind
to libc.

Verified by linking three separately-namespaced Metal runtimes plus their
kernels into one process: previously a duplicate-symbol/shared-state failure, now
links cleanly and keeps per-runtime state independent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@derek-gerstmann

Copy link
Copy Markdown
Contributor Author

The most general solution I could come up with was to grab a snapshot of the runtime module before pipeline methods get injected, and then check their declaration and usage during the renaming pass to handle all globals regardless of the declaration scope.

Derek Gerstmann and others added 5 commits July 27, 2026 11:27
On Windows/COFF, link_modules() places each weak runtime function in a COMDAT
named after the function, and the COFF backend emits associative sections (e.g.
exception-handling data) that reference that COMDAT's leader symbol by name.
The runtime-namespace rename used llvm::GlobalValue::setName() directly, which
renamed the symbol but left its COMDAT keyed by the old name, producing:

    Associative COMDAT symbol 'halide_internal_aligned_alloc' does not exist.

when emitting the namespaced runtime objects.

Add rename_runtime_symbol(), used for every runtime-symbol rename: if the symbol
has a COMDAT keyed by its (old) name, re-key that COMDAT to the new name (same
selection kind) so the leader stays consistent. On targets where COMDATs are
stripped (e.g. macOS) getComdat() is null and this is just a setName().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dows)

Two follow-ups to the runtime-namespace renaming, both surfacing as Windows
link failures when the ISO test links several namespaced runtimes:

1. halide_-prefixed *global variables* were renamed by neither pass: pass (a)
   only iterated functions, and pass (b) skips halide_-prefixed names. Some
   runtime state is exposed with C linkage and a halide_ name (e.g.
   halide_reference_clock_inited in windows_clock.cpp, which is inside
   `extern "C"`, unlike the namespaced posix_clock.cpp used on Mac/Linux). Left
   un-renamed, it stayed identical across runtimes and produced
   "conflicting weak extern definition for 'halide_reference_clock_inited'".
   Pass (a) now also renames halide_-prefixed globals (definition -> export,
   declaration -> import).

2. rename_runtime_symbol now gives every renamed symbol that had a COMDAT its
   own fresh COMDAT named after the new symbol, rather than only re-keying when
   the COMDAT name matched the old symbol name. This dissolves stale shared /
   associative COMDAT groups (e.g. a global grouped with aligned_alloc), keeping
   every COMDAT leader well-defined.

Verified by cross-compiling the runtime for x86-64-windows and inspecting the
COFF object: halide_reference_clock_inited becomes runtime_a_reference_clock_inited
with a matching .weak....default.runtime_a_internal_aligned_alloc, and no bare
.weak.halide_ symbols remain. Mac/Metal isolation still links and runs with
independent per-runtime state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The custom rules for the ISO test's six kernels passed -g/-f but not -n, so the
generator named its outputs after the function (strip_namespaces(-f), e.g.
pipe_none.a) instead of the rule's target (rniso_none.a). The generator ran but
produced differently-named files, so the link step failed with
"no such file or directory: bin/host/build/rniso_none.a".

Pass -n <base> (rniso_none, rniso_a, ... and the _c variants) so the emitted
files match the Make targets. The runtimes already name their output via -r, so
only the kernel rules needed this.

Verified by replaying the exact rule commands against a built generator: the
files are now named rniso_*.{a,h} / rniso_*_c.{halide_generated.cpp,h}, and the
Makefile-style link of all six kernels plus the three namespaced runtimes builds
and runs the isolation test successfully.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants