Skip to main content
Ctrl+K
AMD Logo
ROCm™ AI Ecosystem
  • Core SDK
  • AI Ecosystem
  • GPU Systems and Infrastructure
  • Toolkits
    • AMD Data Science
    • AMD Finance
    • AMD Life Science
    • AMD LLM Extension
    • AMD Simulation
  • Blogs
  • Developer Hub

ROCm AI Ecosystem

FlyDSL 0.3.2

  • Install FlyDSL
  • Quick start
  • API stability

Language

  • Arithmetic types
  • Composite types
  • Storage and allocator

Guides

  • Architecture and compilation pipeline
  • Layout algebra
  • Kernel authoring
  • Kernel tuning
  • Offline autotune configs
  • Prebuilt kernel library
  • Testing and benchmarking
  • CuTe layout algebra
  • External bitcode integration

API reference

  • FlyDSL Python DSL
  • Compiler and pipeline
  • Prebuilt kernels

Tutorials

  • Basic usage
  • Kernel development techniques

About

  • License
  • External...
  • Repository
  • Show source
  • Open issue
  • .md

External bitcode integration (ffi + link_extern)

Contents

  • 1. The expression-level ffi surface
  • 2. Linking external bitcode
  • 3. How the JIT pipeline picks things up
  • 4. The post-load module capture contract
  • 5. Pickling / on-disk cache contract
  • 6. Related files

External bitcode integration (ffi + link_extern)#

This document describes how a framework (for example, 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 (for example, 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 you cannot legitimately hoist a callable to top-level (for example, an instance method closing over runtime state), you 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/ROCm/FlyRocmRuntimeWrappers.cpp

C++ runtime: stateless mgpuModuleLoad wrapper

previous

CuTe layout algebra reference for FlyDSL

next

FlyDSL Python DSL

Contents
  • 1. The expression-level ffi surface
  • 2. Linking external bitcode
  • 3. How the JIT pipeline picks things up
  • 4. The post-load module capture contract
  • 5. Pickling / on-disk cache contract
  • 6. Related files

  • Terms and Conditions
  • ROCm Licenses and Disclaimers
  • Privacy
  • Trademarks
  • Supply Chain Transparency
  • Fair and Open Competition
  • UK Tax Strategy
  • Cookie Policy
  • Cookie Settings
© 2026 Advanced Micro Devices, Inc