External Functions
ttl.call_extern_func invokes a C++ function declared in a custom header. It
supports static template arguments, runtime function arguments, an optional
typed scalar result, custom include directories, and portable logical-kernel
selection.
ttl.call_extern_func(
header,
callee,
*,
template_args=None,
func_args=None,
dfb_dependencies=None,
dfb_effects=None,
unknown_dfb_access=False,
include_paths=None,
kernel=None,
result_type=None,
condition_result=None,
)
header and callee are compile-time strings. template_args, func_args,
and include_paths preserve source order. The compiler does not validate the
C++ signature.
Scalar results
result_type=ttl.ScalarType.I32 and result_type=ttl.ScalarType.I64 declare
one signless scalar integer result. Omitting result_type or passing None
declares a void function. Raw strings, integers, and the ScalarType class are
invalid result declarations.
predicate = ttl.call_extern_func(
HEADER,
"is_enabled",
result_type=ttl.ScalarType.I64,
kernel=ttl.KernelKind.COMPUTE,
)
if predicate:
ttl.call_extern_func(
HEADER,
"execute_enabled_work",
kernel=ttl.KernelKind.COMPUTE,
)
A scalar-result call may be assigned directly in a unified operation. A call
selected for multiple logical kernels creates an independent local result in
each selected kernel. Enclosing structured control is retained only in logical
kernels that retain work in its regions. Composition preserves captured
ScalarType members and includes them in deterministic operation identity.
Logical-kernel selection
A unified @ttl.operation assigns an external call to one or more logical
kernels with kernel=. KernelKind.COMPUTE and
KernelKind.DATA_MOVEMENT select the compiler-owned canonical kernel of that
kind.
@ttl.operation(grid=(1, 1))
def compute_external(inp):
ttl.call_extern_func(
HEADER,
"compute_entry",
func_args=[ttl.raw_addr(inp)],
kernel=ttl.KernelKind.COMPUTE,
)
Combine canonical kernel kinds with | when one call executes in both:
ttl.call_extern_func(
HEADER,
"shared_entry",
kernel=ttl.KernelKind.COMPUTE | ttl.KernelKind.DATA_MOVEMENT,
)
An operation-local Kernel distinguishes multiple kernels with the same
kind. An operation factory may create one handle and capture it in the
operation and related factory callbacks. Operation registration binds the
same handle in place using its capture name and deterministic operation
identity.
def make_selected_external():
reader = ttl.Kernel(ttl.KernelKind.DATA_MOVEMENT)
@ttl.operation(grid=(1, 1))
def selected_external(inp):
ttl.call_extern_func(
HEADER,
"reader_entry",
func_args=[inp],
kernel=reader,
)
ttl.call_extern_func(
HEADER,
"shared_entry",
kernel=(ttl.KernelKind.COMPUTE, reader),
)
return selected_external
A handle used only by the operation may instead be declared as a static top-level assignment in the operation body.
Explicit multi-kernel operations use the same selectors on thread decorators. Each decorator accepts one selector whose kind matches the thread type. An omitted selector denotes the canonical kernel of that kind.
def make_explicit_operation():
reader = ttl.Kernel(ttl.KernelKind.DATA_MOVEMENT)
@ttl.operation(grid=(1, 1))
def explicit_operation(inp):
@ttl.compute(kernel=ttl.KernelKind.COMPUTE)
def compute_thread():
pass
@ttl.datamovement(kernel=reader)
def reader_thread():
pass
return explicit_operation
Operation registration binds a captured thread selector before compilation. This permits the same handle to identify the compiled kernel in runtime configuration APIs.
Composing a unified operation preserves each callee-owned handle, including
its operation identity. Repeated sequential calls to the same composed
operation share that logical kernel instead of consuming additional target
kernel resources. The original handle therefore remains equal to the
KernelSpec.logical_kernel value produced for the composed program.
Factory-created operations with different immutable nonlocal captures receive
different deterministic operation identities. Equal captures retain the same
identity.
An external call accepts one selector or multiple distinct selectors. The |
syntax combines KernelKind values. A nonempty tuple supports selections that
include operation-local Kernel handles. Multiple selectors emit the call once
in every selected logical kernel. A call may omit kernel= when its enclosing
callback already determines one logical kernel. A top-level opaque call without
a selector is invalid because the compiler cannot infer placement from C++
code.
The target backend assigns logical kernels to its supported kernel resources. Compilation fails when an operation requests more kernels of a kind than the target supports. Unified and explicit multi-kernel operations use the same target capacity table and diagnostic terms.
Dispatch-stable condition results
condition_result= accepts a ttl.DispatchCondition created with
ttl.ScalarType.I32 or ttl.ScalarType.I64. It identifies independent
evaluations of one runtime condition. Create the immutable declaration in an
enclosing operation factory and capture the same object in every evaluation:
def make_conditional_operation():
active = ttl.DispatchCondition(ttl.ScalarType.I64)
producer = ttl.Kernel(ttl.KernelKind.DATA_MOVEMENT)
consumer = ttl.Kernel(ttl.KernelKind.COMPUTE)
@ttl.operation(grid=(1, 1))
def conditional_operation(input_tensor):
producer_active = ttl.call_extern_func(
HEADER,
"evaluate_for_producer",
condition_result=active,
kernel=producer,
)
consumer_active = ttl.call_extern_func(
HEADER,
"evaluate_for_consumer",
condition_result=active,
kernel=consumer,
)
return conditional_operation
Calls using one declaration must return the same truth value for one dispatch
and launch coordinate. Zero is false and nonzero is true. Each evaluation must
be repeat-safe. A condition-result call cannot depend on DFB storage, declare
DFB effects, or set unknown_dfb_access=True. It cannot carry a DFB argument,
index, or descriptor. condition_result supplies the result type and cannot be
combined with result_type.
Composition and logical-kernel splitting preserve declaration identity. Distinct declarations remain independent even when calls have equal C++ names, headers, template arguments, or source text. The compiler also preserves branch polarity, structured nesting, and supported boolean expressions when it uses the identity to prove equal conditional DFB execution. Missing or partial identity remains conservative. In the compiled IR module, equal condition attributes identify one declaration and distinct declarations use distinct ordinals.
Template arguments
template_args accepts compile-time values and explicit DFB wrappers.
Python argument |
Generated C++ argument |
|---|---|
|
Signed integer constant |
|
Boolean constant |
|
Unsigned binary32 bit-pattern constant |
|
|
|
Physical DFB index constant |
A bare DFB is invalid in template_args. ttl.dfb_descriptor supplies typed
allocation metadata. ttl.get_dfb_id supplies only an integer index.
ttl.call_extern_func(
HEADER,
"external_copy",
template_args=[
ttl.dfb_descriptor(source_dfb),
ttl.dfb_descriptor(destination_dfb),
4,
False,
],
kernel=ttl.KernelKind.DATA_MOVEMENT,
)
Function arguments
func_args accepts lowered scalar values, DFBs, base tensors, and raw tensor
addresses.
Python argument |
Generated C++ argument |
Restrictions |
|---|---|---|
Scalar value |
Scalar parameter |
Uses the kernel runtime-argument convention. |
DFB |
Physical DFB index parameter |
Declares a direct dependency on that DFB. |
Base tensor |
|
Supported in data-movement kernels for tiled BF16 and FP32 tensors. |
|
|
Supported in compute and data-movement kernels. |
Tensor slices, views, and computed tensor values are not valid external
arguments. ttl.raw_addr provides no layout, view offset, page size, alignment,
or bounds metadata.
When an external function consumes ttl.get_dfb_id(dfb), the same DFB must be
a dependency through func_args, ttl.dfb_descriptor(dfb), or
dfb_dependencies. An index value does not declare storage access by itself.
DFB dependencies and protocol effects
dfb_dependencies declares DFB storage used by external C++ without adding
C++ function arguments. DFBs in func_args and DFB descriptors in
template_args are dependencies automatically. dfb_dependencies must
contain distinct DFBs that are not already automatic dependencies.
dfb_effects is an optional call-wide list of synchronous DFB protocol actions
in the exact order the external function executes them. Each action explicitly
names one DFB dependency and has a positive, statically resolvable tile count no
greater than the DFB capacity. A complete summary can provide the lifecycle
proof needed for physical-index reuse:
ttl.call_extern_func(
HEADER,
"external_stage",
template_args=[ttl.get_dfb_id(source)],
func_args=[source],
dfb_dependencies=[destination],
dfb_effects=[
ttl.DFBEffect.wait(source, tiles=2),
ttl.DFBEffect.pop(source, tiles=2),
ttl.DFBEffect.reserve(destination, tiles=1),
ttl.DFBEffect.push(destination, tiles=1),
],
kernel=ttl.KernelKind.DATA_MOVEMENT,
)
The example defines one sequence across both DFBs: wait on source, pop
source, reserve destination, then push destination. The per-DFB
subsequences are wait -> pop for source and reserve -> push for
destination. Actions on different DFBs occupy distinct positions in the
call-wide sequence.
The supported actions are ttl.DFBEffect.reserve, push, wait, and pop.
ttl.DFBEffect.repeat(count, effects) repeats a nonempty literal effect
sequence a nonnegative, statically resolvable number of times. The frontend
expands the repeat before creating IR, so downstream analyses receive the same
flat effect sequence as an explicitly written list. The expanded dfb_effects
sequence is limited to 4096 actions per external call. This compiler-resource
limit bounds materialization and analysis work; it is not a hardware limit:
dfb_effects=[
ttl.DFBEffect.repeat(
transaction_count,
[
ttl.DFBEffect.wait(source, tiles=tiles_per_transaction),
ttl.DFBEffect.pop(source, tiles=tiles_per_transaction),
],
),
]
Tile and repeat counts may use integer literals, integer captures, and
module-level integer variables combined with unary + or - and the binary
operators +, -, *, //, and %. Booleans and runtime SSA values are not
static integer counts. Floor-division and modulo divisors must be nonzero.
Every listed action occurs on every execution of the call, and list order is execution order. Conditional actions must use TTL control flow around both the matching acquisition and a call with an unconditional summary, execute unconditionally in external C++, or be omitted so the dependency remains opaque. Repeated transactions retain every action and its position. A bounded lifecycle requires ordered reserve/push and wait/pop transactions with matching tile counts. A partial summary is valid but does not prove a bounded lifecycle for that dependency. A dependency occurrence with no listed effect is an opaque storage access for the complete call duration, including when operand adaptation aliases multiple occurrences to the same SSA DFB. Every aliased occurrence requires its own effects to avoid an opaque call-duration access.
unknown_dfb_access=True declares that external C++ may access user-managed
DFBs not present in the declared dependencies. This is distinct from malformed
metadata. For allocation, the call becomes an opaque occurrence on every
user-managed DFB in each scope where it may execute, including listed DFBs.
Listed dependencies and effects remain available to other verification.
Every listed effect is complete when the external function returns. External work that continues after return requires separate explicit completion semantics and cannot be represented by this synchronous effect list. Effects describe external behavior; they do not emit reserve, push, wait, or pop calls. Dependency-only operands and all effect metadata leave the generated C++ call signature unchanged.
The IR stores each effect as a generated enum and a typed attribute. Its
dependency index identifies an element of the value sequence returned by the
call’s getDFBDependencyOperands() interface method. Operation adaptation may
map distinct occurrences to the same DFB without merging them. Separate
executable operations would misrepresent actions already performed in C++,
while integer or string dictionaries would permit untyped effect kinds.
Callee-name, header-name, and generated-C++ inspection do not provide a semantic
contract and are not used.
Include directories
include_paths contains compile-time directory strings added to external
header lookup. The compiler emits the requested header before the call.
ttl.call_extern_func(
"kernels/custom_entry.hpp",
"custom_entry",
include_paths=[PROJECT_INCLUDE_DIR],
kernel=ttl.KernelKind.COMPUTE,
)
DFB synchronization ownership
External C++ must complete its resource accesses before returning. The compiler
does not infer reserve, wait, push, or pop operations from the C++ body; the
dfb_effects contract supplies those facts when required.
TensorBlock.push and TensorBlock.pop accept one kernel= selector when a
DFB transaction has no other use from which ownership can be inferred.
unused = input_dfb.wait()
unused.pop(kernel=ttl.KernelKind.DATA_MOVEMENT)
reserve and wait do not accept kernel=. Their logical-kernel ownership is
derived from the acquired block’s uses and release. An explicit release
selector that conflicts with inferred ownership is invalid.
The release selector affects unified-operation splitting only. Generated IR
and C++ retain the ordinary DFB push or pop signature; kernel is not an
emitted operand or argument. An explicit thread may use the same signature, but
its thread decorator already determines ownership, so the release selector has
no additional effect.