Skip to content

Latest commit

 

History

History
135 lines (105 loc) · 6.51 KB

extern_integration_guide.md

File metadata and controls

135 lines (105 loc) · 6.51 KB

External bitcode integration (ffi + link_extern)

This document describes how a framework (e.g. mori's shmem device API) plugs its pre-compiled LLVM bitcode into FlyDSL's JIT pipeline and participates in post-load device-side initialisation, without FlyDSL's compiler ever importing the framework.

For the mori-side view — cold-start cost, ABI metadata, the three-piece contract, and user-level @flyc.kernel examples — see mori/python/mori/ir/flydsl/README.md.

1. The expression-level ffi surface

flydsl.expr.extern.ffi emits an llvm.call to an external C symbol inside a @flyc.kernel body. It is intentionally link-agnostic and mirrors a normal expression builder: declare the external prototype if needed, then emit the call at the current insertion point.

Parameter Purpose
symbol Mangled C symbol in the external library
arg_types, ret_type MLIR-friendly type names ("int32", "uint64", "void", …)
is_pure Metadata for future lowering to llvm.func readnone / willreturn attributes

Frameworks can pre-construct ffi wrappers for their device ABI and expose them as module-level callables.

2. Linking external bitcode

flydsl.compiler.extern_link.link_extern attaches compilation/runtime metadata to a pure ffi callable:

from flydsl.expr.extern import ffi
from flydsl.compiler.extern_link import link_extern

my_pe = link_extern(
    ffi("mori_shmem_my_pe", [], "int32"),
    bitcode_path=get_bitcode_path(),
    module_init_fn=shmem_module_init,
)

The wrapper registers:

  • bitcode_path in CompilationContext.link_libs, which is fed to rocdl-attach-target l=<path> so external symbols are resolvable during GPU binary generation.
  • module_init_fn in CompilationContext.post_load_processors, which is invoked once per loaded hipModule_t.

flydsl.expr.extern.ExternFunction is the same pure FFI callable exposed as ffi. Integrations that need external bitcode or post-load initialization should explicitly wrap it with link_extern(...).

3. How the JIT pipeline picks things up

Each call to a linked extern inside a @flyc.kernel body first registers its link metadata on the active CompilationContext, then delegates to the underlying ffi callable to emit the llvm.call.

JitFunction.__call__ snapshots link_libs and post_load_processors and hands them to MlirCompiler.compile(..., link_libs=...) and CompiledArtifact(post_load_processors=...) respectively.

The compiler path never imports the framework — everything flows through CompilationContext. Adding a new framework (Triton-on-FlyDSL, a custom in-house DSL, …) only requires building matching ffi + link_extern wrappers.

4. The post-load module capture contract

module_init_fn typically writes runtime pointers into device-side globals (e.g. mori's globalGpuStates) that the framework's bitcode relies on. Triggering it at exactly the right moment requires cooperation with the runtime. FlyDSL installs a custom GPU offloading handler, #fly.explicit_module, on JIT GPU modules. During LLVM translation this handler emits lookup-able flydsl_gpu_module_init and flydsl_gpu_module_load_to_device functions instead of relying on a global constructor. The Python executor calls those functions explicitly and owns the returned hipModule_t handles.

The short version:

  • The loaded-module list is owned by one GpuJitModule instance.
  • There is no global or thread-local module-load callback in the C++ runtime.
  • Multiple Python threads can JIT concurrently because each compiled artifact exposes and calls its own FlyDSL ROCm module loader functions.

On the Python side, jit_executor.py::CompiledArtifact._ensure_engine enforces a post-condition: if any post_load_processors were registered but explicit module loading produced zero observed module loads, it raises RuntimeError immediately. This turns a silent contract violation into a loud, top-of-stack failure instead of letting the first kernel launch fault on uninitialised device globals.

5. Pickling / on-disk cache contract

CompiledArtifact is pickleable for on-disk JIT caching. The serialisation rules are:

  • ffi / linked extern instances are never pickled — they are module-level callables reachable via normal import/attribute access.
  • post_load_processors callables are serialised as "module:qualname" strings and re-imported on cache hit. Lambdas, functools.partial, and bound methods cannot be represented and will cause __getstate__ to raise pickle.PicklingError at cache-write time.
  • Extern-linked artifacts are not written to the on-disk cache. Their external bitcode is a compilation input, so the in-memory cache is used for same-process reuse while avoiding stale fatbins across processes.

Silent drops are intentionally not allowed: a cached kernel that round-tripped without its initialiser would later GPU-fault on uninitialised device globals, with a stack that gives no hint about the missing processor. Failing loudly at pickle time shifts that diagnostic from production into the development cycle.

If a callable cannot legitimately be hoisted to top-level (e.g. an instance method closing over runtime state), the caller should either:

  1. wrap it in a thin top-level function that re-acquires the state on each call, or
  2. suppress the disk-cache write path for that specific artifact and rely on the in-memory cache only.

6. Related files

File Role
python/flydsl/compiler/extern_link.py link_extern, linked extern wrapper, resolver registration
python/flydsl/expr/extern.py Pure FFI ExternFunction class + llvm.call emitter
python/flydsl/compiler/kernel_function.py CompilationContext (carries link_libs, post_load_processors)
python/flydsl/compiler/jit_function.py Passes link_libs into MlirCompiler.compile and propagates post_load_processors to CompiledArtifact
python/flydsl/compiler/jit_executor.py Looks up FlyDSL ROCm module loader symbols, owns GpuJitModule, and runs post_load_processors
lib/Runtime/FlyRocmRuntimeWrappers.cpp C++ runtime: stateless mgpuModuleLoad wrapper