Use TLE-Lite#
This section introduces how to use TLE-Lite. TLE-Lite is available on trition_3.6.x branch.
Memory management#
You can use the following operations to manage the memory.
tle.load#
tle.load loads a tensor asynchronously from GMEM. It supports asynchronously hint.
x = tle.load(..., is_async=True)
Tensor slicing#
Splits the input tensor into a grid of sub-tiles based on the specified sub-tile shape, and extracts the sub-tile at the given coordinates. GPU: Supports extraction into registers and shared memory.
tle.extract_tile#
Splits the input tensor into a grid of sub-tiles based on the specified sub-tile shape, and extracts the sub-tile at the given coordinates.
Supports extraction into registers and shared memory.
# x is [4, 4]
# z is [2, 2]
# Split x into a sub-tile grid with shape=[2, 2], and extract the sub-tile at [0, 0]
z = x.extract_tile(index=[0, 0], shape=[2, 2])
tle.insert_tile#
Splits the input tensor into a grid of sub-tiles based on the sub-tile shape, and updates the sub-tile at the specified coordinates with a new tile.
Supports updates from registers and shared memory.
# x is [4, 4], y is [2, 2], z is [4, 4]
# Split x into sub-tiles of shape=[2, 2], update the [0, 0] sub-tile with y, and return the full [4, 4] tensor
z = x.insert_tile(y, index=[0, 0])
Scan and sort Ops#
Scan and sort Ops provide partial tensor primitives such as prefix, rank, and selection, suitable for histogram-based top-k, stream compaction, and block-level sorting and bucketing scenarios.
TLE-Lite keeps these operations as high-level semantics rather than binding them to a specific hardware implementation: users describe the scan and sort intent, and the backend selects register or shared memory lowering strategies based on the hardware.
tle.cumsum#
tle.cumsum(input, axis=0, reverse=False, dtype=None) computes exclusive cumulative sum and total sum along the axis dimension in one operation.
Signature:
tle.cumsum(input, axis=0, reverse=False, dtype=None)Purpose: Uses a single semantic scan op to compute both the exclusive prefix/suffix sum and total sum of a block tensor.
Returns:
(exclusive_sum, total_sum).Typical scenarios: top-k, histogram prefix, stream compaction, and block-level partition logic requiring partial rank/offset.
exclusivehas the same shape asinput;totalis the scalar sum of the scanned block.reverse=Trueindicates a reversed exclusive sum, suitable for suffix count in descending radix/top-k selection.dtypecan explicitly control the accumulation/result type. By default, narrow integers are promoted to 32-bit integers, and bfloat16 is promoted to float32.For inclusive cumulative sum, use
exclusive_sum + input.Use explicit mask loads for invalid lanes and set inactive lanes to 0, ensuring
total_sumonly counts valid elements.Supported scope is static rank-1 block tensors with
axis=0; this covers the histogram and radix-selection workloads already used by TLE top-k kernels.
Simple example:
exclusive, total = tle.cumsum(x, axis=0)
inclusive = exclusive + x
Pipeline#
Pipe and stage#
tle.pipe describes an explicit dataflow edge between a producer and one or more consumers. It simultaneously records the shared-memory stage holding the logical chunk and the synchronization required to make that chunk visible to consumers, enabling CTA-level load/compute overlap and warp-specialized producer/consumer code to use a typed descriptor instead of manually writing multiple barriers.
Signature:
tle.pipe(*, capacity, scope="cta", name=None, readers=None, one_shot=False, **fields)Purpose: Creates a typed pipe for explicitly describing CTA-level producer/consumer dataflow, ring-buffer stage reuse, and synchronization edges.
Parameters:
capacity: Compile-time positive integer indicating the number of pipe stages; each payload field’s first dimension must equalcapacity.scope: Supported value is"cta".name: Optional pipe name for IR/diagnostics; must be a string if provided.readers: Optional list of reader names; omitted means default SPSC reader; passed as("left", "right")for SPMC.one_shot: Whether this is a single ready/full edge; suitable for startup data broadcast.one_shot=Truedoes not supportclose.**fields: One or more payload buffers, which must be shared-memory buffered tensors returned bytle.gpu.alloc(..., scope=tle.gpu.smem), with rank >= 2.
Naming rules:
Pipe field names and reader names must be valid Python identifiers.
Names must not start with
_.fieldsandreadersare reserved names.
tle.pipe(...)returns a pipe descriptor. It owns staged payload fields and creates producer/consumer endpoints viawriter()andreader(...).capacitystages form a ring buffer.itermaps tostage = iter % capacity, using a phase bit to distinguish reuse rounds.
Producer#
The producer holds pipe.writer(). It acquires a writable stage, fills all necessary fields for the logical chunk, and then commits the chunk, making the data observable to consumers.
pipe_value.writer()→pipe_writer: Creates the single writer endpoint for the current pipe.The writer always sees all payload fields.
writer.acquire(iter)→pipe_slot: Acquires a stage writable by the producer, returning a slot with the leadingcapacitydimension removed.Users should produce field data between
writer.acquire(iter)andwriter.commit(iter).writer.commit(iter)→None: Marks the stage as ready, visible to subscribing consumers. All field writes for the same logical chunk must complete before commit.writer.close(iter)→None: Publishes a closed stage for close-aware consumer loops to exit or switch state. Pipes withone_shot=Truedo not supportclose.Commit is the producer-side visibility boundary.
Consumer#
The consumer holds pipe.reader(...). It waits for published chunks, reads the returned slot, and releases the stage after all reads are complete.
pipe_value.reader(name=None, fields=None)→pipe_reader: Creates a consumer endpoint.For SPSC pipes (
readers=None),namemust be omitted.For SPMC pipes (e.g.,
readers=("mma", "epilogue")),namemust be passed and match a declared reader.fieldscan be a non-empty, compile-time tuple/list of unique payload field names; omitted means subscribing to all fields.Field-subset consumers only narrow the endpoint view and
wait().slot; they do not create a new pipe.reader.wait(iter)→pipe_wait_result: Waits for a stage to be ready or closed, returning the slot and closed flag.Standard consumption paths read
wait_result.slot; checkwait_result.is_closedonly when handling closure.reader.release(iter)→None: Releases the stage after consumption, allowing the producer to reuse it. Should be called after allwait(iter).slotreads are complete.Wait is the consumer-side visibility boundary; release is the consumer-side release signal.
Payload fields#
**fieldsdefines the data carried by each stage. Each field is exposed on thepipe_slotby name, e.g.,slot.qorslot.scale.pipe_slotalso exposesfields: dict[str, tle.gpu.buffered_tensor].pipe_wait_resultcontainsslot: pipe_slotandis_closed: tl.tensor.A pipe can carry one or multiple fields. When splitting pipes, split by logical lifecycle and reader protocol, not by underlying transport.
Different fields in the same slot can be produced by different mechanisms, such as TMA copy, cp.async-style copy, or
tle.gpu.local_ptr+tl.store. Users still callwriter.commit(iter)once after producing all fields for that logical chunk.Each field’s transport is inferred by the compiler from the producer-side IR; it is not a pipe attribute the user fills in, nor should it be encoded into pipe names, field names, or extra user attributes.
When a reader only consumes a subset of fields, use
pipe.reader(name, fields=(...))to narrow the reader view; this does not create a new token.Keep pipe-field provenance visible. Opaque shared-memory pointer escapes, untracked shared stores, or overlapping writes that cannot be proven safe will error directly, without silent fallback.
NVIDIA lowering maps CTA-scoped SMEM pipes to NVWS/mbarrier synchronization. Multi-field payloads require proof of payload window, field ownership, participant count, and source-order safety at the pipe-field root granularity.
Lifecycle#
SPSC pipe represents one producer publishing to one default consumer.
SPMC pipe represents one producer publishing the same logical chunk to multiple named consumers, e.g.,
("mma", "epilogue").iteris the logical chunk ID. Within the same chunk, the producer and all participating consumers should use the sameiter.The standard loop lifecycle is
writer.acquire(iter)→ produce fields →writer.commit(iter)→reader.wait(iter)→ consume fields →reader.release(iter).one_shot=Trueindicates a single ready/full edge, typically used withcapacity=1; do not rely on ring reuse orclosein this mode.
Simple example#
Automatic software pipelining can still be triggered by tl.range(..., num_stages=...). Explicit pipes are suited for scenarios where producer/consumer splitting needs to be visible in the program.
stage_buf = tle.gpu.alloc([2, BLOCK], dtype=tl.float32, scope=tle.gpu.smem)
pipe = tle.pipe(capacity=2, scope="cta", name="x_pipe", x=stage_buf)
writer = pipe.writer()
reader = pipe.reader()
offs = tl.arange(0, BLOCK)
slot = writer.acquire(k)
tl.store(tle.gpu.local_ptr(slot.x), tl.load(x_ptr + k * BLOCK + offs))
writer.commit(k)
ready = reader.wait(k)
x = tl.load(tle.gpu.local_ptr(ready.slot.x))
reader.release(k)
Distribution#
The Triton distributed API consists of four core parts: device mesh definition, sharding specification description, synchronization, and remote access (point-to-point communication).
device mesh#
tle.device_mesh#
tle.device_mesh defines the topological structure of physical devices. It is the fundamental context for all distributed operations.
class device_mesh:
def __init__(self, topology: dict):
"""
Initialize a DeviceMesh.
Args:
topology (dict): A dictionary describing the hardware hierarchy.
Keys are level names; values are either an integer (for 1D)
or a list of tuples (for multi-dimensional levels).
"""
self._physical_ids = ... # Internal storage: flattened list of physical IDs (0..N-1)
self._shape = ... # Shape of the current logical view, e.g., (2, 2, 4, 2, 2, 4)
self._dim_names = ... # Names of the current dimensions
# Initialization and parsing logic...
@property
def shape(self):
"""Return the logical shape of the current mesh."""
return self._shape
@property
def ndim(self):
"""Return the number of dimensions."""
return len(self._shape)
def flatten(self):
"""
Flatten the mesh into 1D. Commonly used for ring-based communication patterns.
"""
return self.reshape(prod(self._shape))
def __getitem__(self, key):
"""
Support slicing operations and return a sub-mesh.
Supports standard slices (slice objects) and integer indexing.
"""
# Compute new shape and selected physical IDs after slicing
# ...
return sub_mesh
def __repr__(self):
return f"DeviceMesh(shape={self._shape}, names={self._dim_names})"
# Define a complex hardware hierarchy
topology = {
# Inter-node level (2x2 = 4 nodes)
"node": [("node_x", 2), ("node_y", 2)],
# Intra-node GPUs (4 devices)
"device": 4,
# Intra-GPU clusters (2x2)
"block_cluster": [("cluster_x", 2), ("cluster_y", 2)],
# Blocks within each cluster (4 blocks)
"block": 4
}
# mesh.shape -> (2, 2, 4, 2, 2, 4)
# Total size = 256
mesh = tle.device_mesh(topology=topology)
Synchronization#
In complex distributed operators—such as Ring-AllReduce or pipelined execution with independent row/column communication—we often need to synchronize only thread blocks within the same “row” or “column,” rather than across the entire cluster. A global synchronization would introduce unnecessary waiting overhead. This API supports sub-mesh synchronization, meaning that within a large physical cluster, we can define multiple logical “communication groups” and perform synchronization independently within each group.
def distributed_barrier(mesh):
"""
If a sub-mesh is passed, only devices within that sub-mesh are synchronized.
Devices outside the sub-mesh should treat this instruction as a no-op
(or the compiler should ensure their control flow never reaches this point).
"""
pass
tle.distributed_barrier#
tle.distributed_barrier synchronize only the set of devices corresponding to the given mesh or sub-mesh.
Read from neighboring shards (ring-style exchange).
node_rank = tle.shard_id(mesh, "node")
device_rank = tle.shard_id(mesh, "device")
next_device = (device_rank + 1) % mesh.shape[1]
remote_x = tle.remote(x, shard_id=(node_rank, next_device), scope=mesh)
tle.distributed_barrier(mesh)
neighbor_vals = tl.load(remote_x)
Remote access#
tle.remote is used to obtain a handle to a tensor located on another device. This corresponds to point-to-point communication or direct memory access (e.g., RDMA/NVLink Load). It enables kernels to explicitly access data from a specific shard.
def remote(tensor, shard_id, scope):
"""
Obtains a handle to a Remote Tensor residing on a specific device shard.
:param tensor: A logically distributed tensor (already annotated with tle.sharding).
:param shard_id: tuple. The coordinates of the target device within the Device Mesh.
For example, if mesh=(2,4) and shard_id=(0, 3), this refers to GPU #3 on node #0.
:return: RemoteTensor. Supports operations such as load, store, etc.
"""
tle.remote: Explicitly read from or write to remote shards.
node_rank = tle.shard_id(mesh, "node")
device_rank = tle.shard_id(mesh, "device")
next_device = (device_rank + 1) % mesh.shape[1]
remote_x = tle.remote(x, shard_id=(node_rank, next_device), scope=mesh)
tle.distributed_barrier(mesh)
neighbor_vals = tl.load(remote_x)
Primitives interactive with local_ptr#
The following APIs are used together with tle.gpu.local_ptr. For more information, see Use TLE-Struct.
tl.load(for local_ptr)tl.store(for local_ptr)tl.atomic_add/and/cas/max/min/or/xchg/xor(for local_ptr)