Operators and declarations¶
An operator class declares what it takes in its body: its parameters
(param), the choices a device resolves (tunable), its operands (In,
Out, InOut) and the values each call supplies (per_call). The class
body is read into a Declaration when it finishes, so the shapes, the cache
keys and the per-call words all come from one place. An operator library
subclasses Operator with library=True and adds building, tuning and
dispatch; amd/IRON's iron.common.declare.Operator does
(amd/IRON#222).
Operator and Unresolvable live in aie.utils.compile.jit.operator;
param, tunable, per_call and Select in aie.utils.compile.jit.params;
In, Out and InOut are exported from aie.utils.compile.jit.
from aie.iron.device import NPU2
from aie.utils.compile.jit import In, Out
from aie.utils.compile.jit.operator import Operator
from aie.utils.compile.jit.params import param, per_call, tunable
class Scale(Operator):
M: int = param()
factor: float = param(default=1.0)
tile: int | None = tunable()
x = In(M, obj_shape=tile)
y = Out(M)
count = per_call(derive=lambda op: op.M // 4)
def resolve(self, dev):
return self.replace(tile=self.tile or 16)
device = NPU2()
op = Scale(M=1024) # Scale(M=1024, factor=1.0, tile=None)
done = op.resolved(device) # Scale(M=1024, factor=1.0, tile=16)
replace returns a copy with the parameters it names changed. A parameter
the caller never gave, computed by a derive or a function default, is
computed again from the new values; one the caller gave, a derive one
included, keeps its value (pass None to compute it again).
In, Out and InOut take the same arguments: the host dims (integers,
param()s and Selects), then dtype, obj_shape (what one ObjectFifo
element holds), per (the param() the stream is split over), depth,
replicate, when (a bool param() the tensor exists under) and like
(an input whose shape this one takes on each call).
A per-call value is an expression in one integer the host gives each call
(PerCall and Affine in aie.helpers.taplib). The host evaluates it, and
a pattern that needs it on the device stages it as arith.
Operator¶
Operator ¶
An operator. Subclass it; each subclass's body is its declaration.
Attributes:
| Name | Type | Description |
|---|---|---|
declaration |
Declaration
|
What the class declares. |
reference_only |
bool
|
The operator has a reference and no kernel yet. |
trace |
Any
|
What the build traces, or None. |
Source code in python/utils/compile/jit/operator.py
validate ¶
compatible ¶
Check the parameters against the resolved tunables; raise ValueError.
Runs on the copy resolved returns.
resolve ¶
Return a copy with every tunable filled for dev.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dev
|
Device
|
The device the tunables are chosen for. |
required |
Returns:
| Type | Description |
|---|---|
Self
|
The resolved copy. |
Raises:
| Type | Description |
|---|---|
Unresolvable
|
No legal value exists on |
Source code in python/utils/compile/jit/operator.py
resolved ¶
Return this operator resolved for dev, which is current while it resolves and is checked.
One already resolved for dev is itself. One that came from an
unresolved operator, by resolving or by replace of a resolved one,
is resolved for dev from that operator.
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Unresolvable
|
|
Source code in python/utils/compile/jit/operator.py
replace ¶
Return a copy with changes, which may name any parameter.
A parameter not in changes that a derive or a function default
computed is computed again; one a caller gave, a derive one
included, keeps its value (give it None to compute it again). The
copy is not resolved, but its keys keep naming the device its
tunables were resolved for, and a copy of a resolved operator
resolves for another device from the unresolved one, with
changes applied, as the original does.
Source code in python/utils/compile/jit/operator.py
copy ¶
Return a fresh instance with every value, still resolved as this one is.
layout ¶
Return this operator as its cores see it: the key's parameters and the members.
Source code in python/utils/compile/jit/operator.py
array_key ¶
Return the identity two operators share their cores by.
It names the device this operator is resolved for (None until resolved), since the resolved tunables depend on it.
Source code in python/utils/compile/jit/operator.py
design_key ¶
Return the identity two operators share a build by: the device and every parameter.
Source code in python/utils/compile/jit/operator.py
Unresolvable ¶
Bases: ValueError
No legal resolution exists for this operator on this device.
Parameters and per-call values¶
The words an operator's class body declares its values with.
M: int = param() is a compile-time parameter, tile: int = tunable() one
a tuner may choose, and n = per_call(np.int32) a value that may change
every call. Each stays the class attribute it was written as, so In(M, K)
captures M itself and GEMM.M names the parameter. On an instance,
op.M is the integer, held in the instance's own __dict__.
OperatorParam ¶
What param() and tunable() return: a parameter of its class.
Attributes:
| Name | Type | Description |
|---|---|---|
default |
The value when the caller gives none, a function of the
operator computing it, or |
|
derive |
A function of the operator a given value must equal, and the value when none is given; or None. |
|
array |
The cores read it, though no operand's shape names it. |
|
repr |
It is in the operator's repr and device symbols. |
|
init |
It is a constructor argument. |
|
tunable |
A tuner may choose it. |
|
domain |
A tunable's search space, or None. |
Source code in python/utils/compile/jit/params.py
Select
dataclass
¶
Select(
flag: OperatorParam, when_true: tuple, when_false: tuple
)
A shape chosen by a flag the caller gives or defaults, never inferred.
Select(b_col_maj, (N, K), (K, N)).
branch ¶
Return the dimensions the flag picks on op.
Source code in python/utils/compile/jit/params.py
param ¶
param(
*,
default: Any = MISSING,
derive: Callable[[Any], Any] | None = None,
array: bool = False,
repr: bool = True,
init: bool = True
) -> Any
Declare a compile-time parameter.
The caller gives it, or the operands' shapes infer it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
default
|
Any
|
The value when neither the caller nor an operand gives one.
A |
MISSING
|
derive
|
Callable[[Any], Any] | None
|
A function of the operator: the value when none is given, and what a given one must equal. |
None
|
array
|
bool
|
The cores read it (a kernel scalar, a flag the build branches on) though no operand's shape, dtype or presence names it. |
False
|
repr
|
bool
|
It is in the operator's repr and device symbols. |
True
|
init
|
bool
|
It is a constructor argument. |
True
|
Returns:
| Type | Description |
|---|---|
Any
|
The |
Raises:
| Type | Description |
|---|---|
TypeError
|
Both |
Source code in python/utils/compile/jit/params.py
tunable ¶
Declare a compile-time parameter a tuner may search.
resolve fills it for the device when the caller does not. The cores
always read it.
With init=False it is no constructor argument: a value resolve
computes, or a subclass's fixed value of an inherited tunable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
default
|
Any
|
The starting value; None means |
None
|
domain
|
Any
|
The values a tuner searches: a tuple, an |
None
|
repr
|
bool
|
It is in the operator's repr and device symbols. |
True
|
init
|
bool
|
It is a constructor argument. |
True
|
Returns:
| Type | Description |
|---|---|
Any
|
The |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Source code in python/utils/compile/jit/params.py
per_call ¶
per_call(
dtype: Any = int32,
*,
derive: Callable[[Any], Any] | None = None,
bounds: OperatorParam | None = None
) -> Any
Declare a value the cores read that may change every call without a rebuild.
A value no build fixes (no derive, no bounds) is given every call:
an operator with a dispatch library takes it as a dispatch scalar,
which rebuilds the instruction stream around it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dtype
|
Any
|
An integer type. |
int32
|
derive
|
Callable[[Any], Any] | None
|
A function of the operator computing it once per build. |
None
|
bounds
|
OperatorParam | None
|
A |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Source code in python/utils/compile/jit/params.py
Operands¶
Argument markers for JIT design parameters.
Five annotation categories are defined here (all exported from aie.iron):
CompileTime[T]
Marks a generator function parameter as compile-time. Changing its value
causes a recompile and a new cache entry. Inspired by tl.constexpr in
Triton. Implemented as Annotated[T, ...] so pyright/mypy check callers
against the wrapped type T itself (e.g. CompileTime[int] behaves
like int for type-checking) while _introspect.py still recovers the
marker at runtime via get_type_hints(..., include_extras=True).
In
Marks a generator function parameter as a runtime input tensor. Data is
DMA-transferred from the host to the NPU on every kernel call.
Out
Marks a generator function parameter as a runtime output tensor. Data is
DMA-transferred from the NPU to the host on every kernel call.
InOut
Marks a generator function parameter as a runtime bidirectional tensor.
Data is DMA-transferred in both directions on every kernel call.
A generator receives each tensor as an operand of its marker's class, never its
data: an In/Out/InOut with the shape and dtype of the
tensor the design is called with, or of a type bound ahead of time with
specialize(name=np.ndarray[...]), as an operator's op.a is. Every API
that takes a tensor type also takes the operand; operand.ndarray_type is
its np.ndarray[shape, np.dtype[T]]. A different shape or dtype is a
different compiled design.
DispatchTime[T]
Marks a keyword-only integer scalar that can vary per dispatch without
recompiling. Explicit specialization instead fixes it at compile time.
See DispatchTime below for generator binding and usage restrictions.
Any parameter without one of these annotations is rejected at @iron.jit
decoration time when the parameter has a default value: an unannotated scalar
is bound at generation time, so the default would be baked into the compiled
kernel and per-call overrides silently ignored. Annotate as
CompileTime[T] (recompiles on change), DispatchTime[T] (runtime
scalar, one compile many values), or In/Out/InOut (DMA tensor)
instead.
In ¶
In(
*dims: Any,
dtype: Any = bfloat16,
obj_shape: Any = None,
per: Any = None,
depth: int = 2,
replicate: bool = False,
when: OperatorParam | None = None,
like: _Operand | None = None
)
Bases: _Operand
Runtime input tensor (host → NPU, DMA each call).
Out and InOut take the same arguments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dims
|
Any
|
The host shape: integers, |
()
|
dtype
|
Any
|
The element type, or a |
bfloat16
|
obj_shape
|
Any
|
What one ObjectFifo element holds. With one the tensor streams into the array; without one a hand-written sequence moves it. |
None
|
per
|
Any
|
The |
None
|
depth
|
int
|
The ObjectFifo depth. |
2
|
replicate
|
bool
|
Every |
False
|
when
|
OperatorParam | None
|
A bool |
None
|
like
|
_Operand | None
|
An input whose dims these are; a caller gives this tensor the
shape that input has on the call, so |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Source code in python/utils/compile/jit/markers.py
Out ¶
Out(
*dims: Any,
dtype: Any = bfloat16,
obj_shape: Any = None,
per: Any = None,
depth: int = 2,
replicate: bool = False,
when: OperatorParam | None = None,
like: _Operand | None = None
)
Bases: _Operand
Runtime output tensor (NPU → host, DMA each call).
Takes the arguments In documents.
Source code in python/utils/compile/jit/markers.py
InOut ¶
InOut(
*dims: Any,
dtype: Any = bfloat16,
obj_shape: Any = None,
per: Any = None,
depth: int = 2,
replicate: bool = False,
when: OperatorParam | None = None,
like: _Operand | None = None
)
Bases: _Operand
Runtime bidirectional tensor (DMA in both directions each call).
Takes the arguments In documents.
Source code in python/utils/compile/jit/markers.py
Declaration¶
Declaration ¶
Declaration(
name: str,
params: dict[str, OperatorParam],
members: tuple[Member, ...],
nullable: frozenset[str] = frozenset(),
scalars: tuple[str, ...] = (),
signature: Signature | None = None,
)
What an operator declares, read from a class body or a generator's signature.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
The class's or the generator's name. |
|
params |
Every compile-time parameter ( |
|
members |
The operands and per-call values, the most derived body's order first. |
|
nullable |
The parameters annotated |
|
scalars |
A generator's parameters with no marker. |
|
signature |
A generator's signature, its annotations evaluated, which binds a call's arguments. |
|
tunables |
The tunables a caller or the tuner sets ( |
|
key |
The parameters the cores are built from: every tunable, what a
stream's |
Source code in python/utils/compile/jit/declaration.py
infer ¶
Return the parameters the operands' shapes give.
Each declared dimension is a param() or a literal, so this is a
lookup. An optional input's when= flag is true where inputs
names it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inputs
|
Mapping[str, Any]
|
The input shapes, by operand name. |
required |
outputs
|
Sequence[Any]
|
The shapes of caller-supplied outputs, in declaration order. |
()
|
**kwargs
|
Any
|
Construction arguments; the dimensions and flags among them pin values and are checked for agreement. |
{}
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
dict[str, Any]
|
the operand flags. |
Raises:
| Type | Description |
|---|---|
TypeError
|
The operands are not the declared ones. |
ValueError
|
A shape disagrees with the declaration or another operand. |
Source code in python/utils/compile/jit/declaration.py
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | |
Per-call expressions¶
PerCall ¶
An integer scalar given per call.
Integer arithmetic on one makes an Affine. marker says how a call
gives it, and only its library reads it: "aie.dispatch_time" for a
DispatchTime parameter. ssa is its staged value once a runtime
sequence binds it.
Source code in python/helpers/taplib/_per_call.py
Affine
dataclass
¶
Affine(
value: PerCall,
scale: int = 1,
bias: int = 0,
down: int = 0,
mul: int = 1,
add: int = 0,
lo: int | None = None,
hi: int | None = None,
mod: int | None = None,
post: int = 0,
)
An integer expression in one per-call value v.
>> floors, a bound of None is none, and mod is a power of two
(a ring's row) or None. post is added after the wrap, so a run of a
ring written twice over, ring[a : a + R], has its end. A linear one
has down 0 and keeps its terms in scale and bias:
(p + 1) * 64 is Affine(p, 64, 64).
Sums and products with integers, floor divisions by a power of two (so
ceildiv), sums of two linear ones in one value, and the quotient of
two that are multiples of one another ((4 * n) // n is 4) are
expressions too. A clamp is carried through a sum, a product or a
floor division by an integer. Anything else (a comparison, another
divisor, two values) raises TypeError.
affine ¶
affine() -> Affine
Return this expression where only a linear one will do.
Raises:
| Type | Description |
|---|---|
TypeError
|
It floors, is clamped or wraps. |
Source code in python/helpers/taplib/_per_call.py
clamp ¶
clamp(
lo: int | None = None, hi: int | None = None
) -> Affine | int
Return this expression held to [lo, hi] too, a bound of None none.
Held to a range it never reaches, it is that range's nearer bound.
Raises:
| Type | Description |
|---|---|
TypeError
|
The expression wraps. |
ValueError
|
|
Source code in python/helpers/taplib/_per_call.py
evaluate ¶
Its value where values gives its per-call value by name.
Source code in python/helpers/taplib/_per_call.py
stage ¶
Stage this expression on its value's i64, as eudsl's ScalarValue.
Raises:
| Type | Description |
|---|---|
TypeError
|
No runtime sequence has bound the value. |