Skip to content

Commit 6fd6c7b

Browse files
mawad-amdclaudegithub-actions[bot]
authored
Add Gluon flat-2D all-gather kernel
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent fe7082b commit 6fd6c7b

4 files changed

Lines changed: 402 additions & 49 deletions

File tree

examples/25_ccl_all_gather/example.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
Each rank contributes an (M, N) tensor; every rank receives the concatenated (world_size*M, N) result.
99
1010
Run with:
11-
torchrun --nproc_per_node=<num_gpus> --standalone example.py [--validate]
11+
torchrun --nproc_per_node=<num_gpus> --standalone example.py [--validate] [--use_gluon]
1212
"""
1313

1414
import argparse
@@ -37,6 +37,7 @@ def parse_args():
3737
parser.add_argument("--num_stages", type=int, default=1, help="Number of stages")
3838
parser.add_argument("--num_warps", type=int, default=4, help="Number of warps")
3939
parser.add_argument("--waves_per_eu", type=int, default=0, help="Number of waves per EU")
40+
parser.add_argument("--use_gluon", action="store_true", help="Use Gluon kernel backend")
4041
return vars(parser.parse_args())
4142

4243

@@ -47,7 +48,12 @@ def main():
4748
torch.cuda.set_device(local_rank)
4849
dist.init_process_group(backend="gloo")
4950

50-
ctx = iris.iris(heap_size=args["heap_size"])
51+
if args["use_gluon"]:
52+
import iris.experimental.iris_gluon as iris_gluon
53+
54+
ctx = iris_gluon.iris(heap_size=args["heap_size"])
55+
else:
56+
ctx = iris.iris(heap_size=args["heap_size"])
5157
rank = ctx.get_rank()
5258
world_size = ctx.get_num_ranks()
5359

@@ -67,6 +73,7 @@ def main():
6773
"num_stages": args["num_stages"],
6874
"num_warps": args["num_warps"],
6975
"waves_per_eu": args["waves_per_eu"],
76+
"use_gluon": args["use_gluon"],
7077
}
7178
config = Config(**config_kwargs)
7279

iris/ccl/all_gather.py

Lines changed: 274 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212
from .config import Config
1313
from .utils import extract_group_info
1414

15+
# Conditional import for Gluon
16+
try:
17+
from triton.experimental import gluon
18+
from triton.experimental.gluon import language as gl
19+
from iris.experimental.iris_gluon import IrisDeviceCtx
20+
21+
GLUON_AVAILABLE = True
22+
except ImportError:
23+
GLUON_AVAILABLE = False
24+
1525

1626
@triton.jit()
1727
def persistent_all_gather(
@@ -279,6 +289,158 @@ def persistent_all_gather_partitioned(
279289
)
280290

281291

292+
# Gluon implementation: flat-2D tiling approach
293+
#
294+
# Uses a single 1D arange over BLOCK_SIZE_M * BLOCK_SIZE_N elements with
295+
# div/mod to compute 2D row/col indices. This gives one load + world_size
296+
# stores per tile (matching Triton's 2D load/store structure) while staying
297+
# within gluon's 1D BlockedLayout framework.
298+
#
299+
# Key optimizations:
300+
# - Flat-2D tiling: eliminates the inner BLOCK_SIZE_M row loop
301+
# - Hoisted pointer translation: local_base loaded once outside tile loop
302+
# - Traffic shaping: staggered write order avoids memory controller contention
303+
if GLUON_AVAILABLE:
304+
305+
@gluon.jit
306+
def persistent_all_gather_gluon(
307+
IrisDeviceCtx: gl.constexpr,
308+
context_tensor,
309+
input_ptr,
310+
output_ptr,
311+
M,
312+
N,
313+
stride_in_m,
314+
stride_in_n,
315+
stride_out_m,
316+
stride_out_n,
317+
group_rank: gl.constexpr,
318+
iris_rank: gl.constexpr,
319+
world_size: gl.constexpr,
320+
rank_start: gl.constexpr,
321+
rank_stride: gl.constexpr,
322+
BLOCK_SIZE_M: gl.constexpr,
323+
BLOCK_SIZE_N: gl.constexpr,
324+
GROUP_SIZE_M: gl.constexpr,
325+
COMM_SMS: gl.constexpr,
326+
THREADS_PER_WARP: gl.constexpr,
327+
WARPS_PER_CTA: gl.constexpr,
328+
):
329+
"""
330+
Persistent all-gather kernel using Gluon with flat-2D tiling.
331+
332+
Uses a flat 1D index space of BLOCK_SIZE_M * BLOCK_SIZE_N elements,
333+
computing 2D row/col via integer div/mod. This produces one vectorized
334+
load and world_size vectorized stores per tile, matching Triton's 2D
335+
load/store instruction structure while staying within gluon's 1D
336+
BlockedLayout framework.
337+
338+
Memory layout (BlockedLayout):
339+
A 1D BlockedLayout distributes TOTAL_ELEMS = BLOCK_SIZE_M * BLOCK_SIZE_N
340+
elements across the thread hierarchy:
341+
ELEMS_PER_THREAD = TOTAL_ELEMS // (THREADS_PER_WARP * WARPS_PER_CTA)
342+
343+
Each thread handles ELEMS_PER_THREAD contiguous elements in the
344+
flattened row-major order. Row/col are recovered via:
345+
row = flat_idx // BLOCK_SIZE_N
346+
col = flat_idx % BLOCK_SIZE_N
347+
348+
Constraints:
349+
- BLOCK_SIZE_M * BLOCK_SIZE_N must be a multiple of
350+
(THREADS_PER_WARP * WARPS_PER_CTA).
351+
- Optimal tile: 2048-4096 total elements (8-16 per thread).
352+
Larger tiles cause register spilling and performance collapse.
353+
- Recommended: BLOCK_SIZE_M=8, BLOCK_SIZE_N=256 (2048 elems, 8/thread).
354+
355+
Args:
356+
IrisDeviceCtx: Gluon device context class for remote memory operations.
357+
context_tensor: Opaque tensor holding IrisDeviceCtx state.
358+
input_ptr: Pointer to local input tensor of shape (M, N).
359+
output_ptr: Pointer to output tensor of shape (world_size * M, N).
360+
M: Number of rows in the input tensor (per rank).
361+
N: Number of columns.
362+
stride_in_m, stride_in_n: Row and column strides for input tensor.
363+
stride_out_m, stride_out_n: Row and column strides for output tensor.
364+
group_rank: This rank's index within the ProcessGroup (0..world_size-1).
365+
iris_rank: This rank's global index in the iris context (for RMA addressing).
366+
world_size: Total number of ranks in the group.
367+
rank_start: First iris rank in the group (for RMA target computation).
368+
rank_stride: Stride between consecutive iris ranks in the group.
369+
BLOCK_SIZE_M: Number of rows per tile.
370+
BLOCK_SIZE_N: Number of columns per tile.
371+
GROUP_SIZE_M: Swizzle group size for M-dimension tiling.
372+
COMM_SMS: Number of CUs used for persistent scheduling.
373+
THREADS_PER_WARP: Threads per warp/wavefront (64 for AMD, 32 for NVIDIA).
374+
WARPS_PER_CTA: Number of warps per workgroup. Must match num_warps.
375+
"""
376+
ctx = IrisDeviceCtx.initialize(context_tensor, tracing=False)
377+
378+
pid = gl.program_id(0)
379+
380+
num_pid_m = gl.cdiv(M, BLOCK_SIZE_M)
381+
num_pid_n = gl.cdiv(N, BLOCK_SIZE_N)
382+
total_tiles = num_pid_m * num_pid_n
383+
384+
# Flat 1D layout covering BLOCK_SIZE_M * BLOCK_SIZE_N elements
385+
TOTAL_ELEMS: gl.constexpr = BLOCK_SIZE_M * BLOCK_SIZE_N
386+
ELEMS_PER_THREAD: gl.constexpr = TOTAL_ELEMS // (THREADS_PER_WARP * WARPS_PER_CTA)
387+
flat_layout: gl.constexpr = gl.BlockedLayout([ELEMS_PER_THREAD], [THREADS_PER_WARP], [WARPS_PER_CTA], [0])
388+
389+
# Hoist local heap base outside the tile loop: eliminates redundant
390+
# gl.load(heap_bases) calls in the inner store loop.
391+
local_base = gl.load(ctx.heap_bases + iris_rank)
392+
393+
for tile_id in range(pid, total_tiles, COMM_SMS):
394+
# Swizzled tile index computation for better L2 locality
395+
num_pid_in_group = GROUP_SIZE_M * num_pid_n
396+
group_id = tile_id // num_pid_in_group
397+
first_pid_m = group_id * GROUP_SIZE_M
398+
group_size_m = min(num_pid_m - first_pid_m, GROUP_SIZE_M)
399+
pid_m = first_pid_m + ((tile_id % num_pid_in_group) % group_size_m)
400+
pid_n = (tile_id % num_pid_in_group) // group_size_m
401+
402+
# Flat index -> 2D row/col within tile
403+
flat_idx = gl.arange(0, TOTAL_ELEMS, layout=flat_layout)
404+
row_local = flat_idx // BLOCK_SIZE_N
405+
col_local = flat_idx % BLOCK_SIZE_N
406+
407+
# Global row/col
408+
row = pid_m * BLOCK_SIZE_M + row_local
409+
col = pid_n * BLOCK_SIZE_N + col_local
410+
411+
mask = (row < M) & (col < N)
412+
413+
# Single flat load of the entire tile
414+
input_offsets = row * stride_in_m + col * stride_in_n
415+
input_addr = input_ptr + input_offsets
416+
data = gl.load(input_addr, mask=mask, other=0.0)
417+
418+
# Output: this rank's data goes to output[group_rank * M + row, col]
419+
output_row = group_rank * M + row
420+
output_offsets = output_row * stride_out_m + col * stride_out_n
421+
422+
# Traffic-shaped stores to all ranks: stagger write order per rank
423+
# so each rank writes to a different target at any given moment,
424+
# avoiding memory controller contention on the receiver side.
425+
for rank_idx in range(world_size):
426+
dest_idx = (group_rank + rank_idx) % world_size
427+
target_iris_rank = rank_start + dest_idx * rank_stride
428+
output_ptrs = output_ptr + output_offsets
429+
430+
if dest_idx == group_rank:
431+
gl.store(output_ptrs, data, mask=mask, cache_modifier=".wt")
432+
else:
433+
# Hoisted translation: compute ptr_delta from pre-loaded
434+
# local_base rather than calling ctx.store() which would
435+
# do 2x gl.load(heap_bases) per call.
436+
target_base = gl.load(ctx.heap_bases + target_iris_rank)
437+
ptr_delta = target_base - local_base
438+
output_ptrs_int = tl.cast(output_ptrs, gl.uint64)
439+
remote_ptrs_int = output_ptrs_int + ptr_delta
440+
remote_ptrs = tl.cast(remote_ptrs_int, output_ptrs.dtype)
441+
gl.store(remote_ptrs, data, mask=mask)
442+
443+
282444
def all_gather(
283445
output_tensor,
284446
input_tensor,
@@ -314,26 +476,11 @@ def all_gather(
314476
if config is None:
315477
config = Config(block_size_m=32, block_size_n=64)
316478

317-
# Check for unsupported options
318-
if config.use_gluon:
319-
raise ValueError(
320-
"all_gather does not support use_gluon=True. "
321-
"Gluon implementation is not available for all_gather. "
322-
"Use default config (use_gluon=False)."
323-
)
324-
325479
# Extract group information
326480
# rank_in_group: position within the ProcessGroup (0, 1, 2, ...) - passed as group_rank to kernel
327481
# rank_global: global rank in iris context - passed as iris_rank to kernel for RMA operations
328482
rank_in_group, rank_global, world_size, rank_start, rank_stride = extract_group_info(group, shmem)
329483

330-
# Validate COMM_SMS divisibility for partitioned variant
331-
if config.all_gather_variant == "partitioned" and config.comm_sms % world_size != 0:
332-
raise ValueError(
333-
f"For all_gather_variant='partitioned', COMM_SMS ({config.comm_sms}) must be divisible by world_size ({world_size}). "
334-
f"Please adjust config.comm_sms to be a multiple of {world_size}."
335-
)
336-
337484
M, N = input_tensor.shape[:2]
338485
expected_output_shape = (world_size * M, N)
339486

@@ -346,41 +493,121 @@ def all_gather(
346493
stride_in_m, stride_in_n = input_tensor.stride(0), input_tensor.stride(1)
347494
stride_out_m, stride_out_n = output_tensor.stride(0), output_tensor.stride(1)
348495

349-
heap_bases = shmem.get_heap_bases()
496+
# Choose between Triton and Gluon implementation
497+
if config.use_gluon and GLUON_AVAILABLE:
498+
# Check if shmem is Iris Gluon (has get_device_context method)
499+
if not hasattr(shmem, "get_device_context"):
500+
raise ValueError("use_gluon=True requires Iris Gluon context. Use iris.experimental.iris_gluon.iris()")
350501

351-
# Dispatch to the appropriate kernel based on variant
352-
if config.all_gather_variant == "persistent":
353-
kernel_fn = persistent_all_gather
354-
elif config.all_gather_variant == "partitioned":
355-
kernel_fn = persistent_all_gather_partitioned
502+
# Gluon only supports the persistent variant
503+
if config.all_gather_variant != "persistent":
504+
raise ValueError(
505+
f"Gluon all_gather only supports all_gather_variant='persistent', got '{config.all_gather_variant}'."
506+
)
507+
508+
# Apply optimal defaults for gluon flat-2D kernel when user hasn't
509+
# overridden block sizes from the Config defaults (32x64).
510+
block_size_m = config.block_size_m
511+
block_size_n = config.block_size_n
512+
if block_size_m == 32 and block_size_n == 64:
513+
# User didn't override — use optimal flat-2D tile: 8x256
514+
block_size_m = 8
515+
block_size_n = 256
516+
517+
# Validate flat-2D layout constraints.
518+
# TOTAL_ELEMS = BLOCK_SIZE_M * BLOCK_SIZE_N must be a multiple of
519+
# THREADS_PER_WARP * WARPS_PER_CTA so each thread gets a whole
520+
# number of elements.
521+
total_elems = block_size_m * block_size_n
522+
threads_per_cta = config.threads_per_warp * config.num_warps
523+
if total_elems < threads_per_cta:
524+
raise ValueError(
525+
f"Gluon all-gather requires block_size_m * block_size_n >= "
526+
f"threads_per_warp * num_warps ({threads_per_cta}), "
527+
f"got {block_size_m} * {block_size_n} = {total_elems}."
528+
)
529+
if total_elems % threads_per_cta != 0:
530+
raise ValueError(
531+
f"Gluon all-gather requires block_size_m * block_size_n to be a "
532+
f"multiple of threads_per_warp * num_warps ({threads_per_cta}), "
533+
f"got {block_size_m} * {block_size_n} = {total_elems}. "
534+
f"Recommended: block_size_m=8, block_size_n=256."
535+
)
536+
537+
context_tensor = shmem.get_device_context()
538+
539+
persistent_all_gather_gluon[(config.comm_sms,)](
540+
IrisDeviceCtx,
541+
context_tensor,
542+
input_tensor,
543+
output_tensor,
544+
M,
545+
N,
546+
stride_in_m,
547+
stride_in_n,
548+
stride_out_m,
549+
stride_out_n,
550+
rank_in_group,
551+
rank_global,
552+
world_size,
553+
rank_start,
554+
rank_stride,
555+
block_size_m,
556+
block_size_n,
557+
config.swizzle_size,
558+
config.comm_sms,
559+
config.threads_per_warp,
560+
config.num_warps,
561+
num_stages=config.num_stages,
562+
num_warps=config.num_warps,
563+
waves_per_eu=config.waves_per_eu,
564+
)
356565
else:
357-
raise ValueError(f"Unknown all_gather_variant: {config.all_gather_variant}")
566+
if config.use_gluon and not GLUON_AVAILABLE:
567+
raise ValueError("Gluon is not available. Install Triton with Gluon support or set use_gluon=False")
568+
569+
# Validate COMM_SMS divisibility for partitioned variant
570+
if config.all_gather_variant == "partitioned" and config.comm_sms % world_size != 0:
571+
raise ValueError(
572+
f"For all_gather_variant='partitioned', COMM_SMS ({config.comm_sms}) must be divisible by world_size ({world_size}). "
573+
f"Please adjust config.comm_sms to be a multiple of {world_size}."
574+
)
358575

359-
kernel_fn[(config.comm_sms,)](
360-
input_tensor,
361-
output_tensor,
362-
M,
363-
N,
364-
stride_in_m,
365-
stride_in_n,
366-
stride_out_m,
367-
stride_out_n,
368-
heap_bases,
369-
rank_in_group,
370-
rank_global,
371-
world_size,
372-
rank_start,
373-
rank_stride,
374-
config.block_size_m,
375-
config.block_size_n,
376-
config.swizzle_size,
377-
config.comm_sms,
378-
config.num_xcds,
379-
config.chunk_size,
380-
num_stages=config.num_stages,
381-
num_warps=config.num_warps,
382-
waves_per_eu=config.waves_per_eu,
383-
)
576+
heap_bases = shmem.get_heap_bases()
577+
578+
# Dispatch to the appropriate kernel based on variant
579+
if config.all_gather_variant == "persistent":
580+
kernel_fn = persistent_all_gather
581+
elif config.all_gather_variant == "partitioned":
582+
kernel_fn = persistent_all_gather_partitioned
583+
else:
584+
raise ValueError(f"Unknown all_gather_variant: {config.all_gather_variant}")
585+
586+
kernel_fn[(config.comm_sms,)](
587+
input_tensor,
588+
output_tensor,
589+
M,
590+
N,
591+
stride_in_m,
592+
stride_in_n,
593+
stride_out_m,
594+
stride_out_n,
595+
heap_bases,
596+
rank_in_group,
597+
rank_global,
598+
world_size,
599+
rank_start,
600+
rank_stride,
601+
config.block_size_m,
602+
config.block_size_n,
603+
config.swizzle_size,
604+
config.comm_sms,
605+
config.num_xcds,
606+
config.chunk_size,
607+
num_stages=config.num_stages,
608+
num_warps=config.num_warps,
609+
waves_per_eu=config.waves_per_eu,
610+
)
384611

385612
if not async_op:
386613
shmem.barrier()

0 commit comments

Comments
 (0)