| |
| |
|
|
| """ |
| Minimal Iris context wrapper for AITER communication operations. |
| |
| This module provides a thin wrapper around the iris library context to |
| support reduce-scatter and all-gather operations. All core iris functions |
| (load, store, put, atomic_*, etc.) are provided by the iris library itself. |
| """ |
|
|
| import logging |
| import math |
| from typing import Optional |
|
|
| import torch |
| import iris |
|
|
| |
| IRIS_AVAILABLE = True |
|
|
| logger = logging.getLogger("aiter") |
|
|
|
|
| def calculate_heap_size( |
| M: int, |
| N: int, |
| dtype: "torch.dtype", |
| world_size: Optional[int] = None, |
| quant_mode: str = "none", |
| all_gather: bool = True, |
| overhead_factor: float = 1.2, |
| ) -> int: |
| """ |
| Calculate required Iris heap size for communication operations. |
| |
| This function estimates the total memory needed in the Iris symmetric heap |
| for reduce-scatter, RMSNorm, quantization, and all-gather operations. |
| |
| Args: |
| M (int): Number of rows in input tensor |
| N (int): Number of columns in input tensor |
| dtype (torch.dtype): Data type of input tensor (e.g., torch.float32, torch.float16) |
| world_size (int, optional): Number of GPUs. If None, uses torch.distributed.get_world_size() |
| quant_mode (str): Quantization mode - "none", "fp8_per_token", or "fp4_per_token" |
| all_gather (bool): Whether all-gather is performed after quantization |
| overhead_factor (float): Safety margin multiplier (default: 1.2 for 20% overhead) |
| |
| Returns: |
| int: Required heap size in bytes |
| |
| Example: |
| >>> # For 2 GPUs, 4096x4096 float32 tensor with FP8 quant and all-gather |
| >>> heap_size = calculate_heap_size(4096, 4096, torch.float32, |
| ... world_size=2, quant_mode="fp8_per_token") |
| >>> with IrisCommContext(heap_size=heap_size) as ctx: |
| ... # Your operations here |
| ... pass |
| """ |
| if world_size is None: |
| if not torch.distributed.is_initialized(): |
| raise RuntimeError( |
| "torch.distributed not initialized and world_size not provided. " |
| "Either initialize torch.distributed or pass world_size explicitly." |
| ) |
| world_size = torch.distributed.get_world_size() |
|
|
| |
| if dtype in (torch.float32, torch.int32): |
| elem_bytes = 4 |
| elif dtype in (torch.float16, torch.bfloat16, torch.int16): |
| elem_bytes = 2 |
| elif dtype in (torch.float64, torch.int64): |
| elem_bytes = 8 |
| elif dtype == torch.int8: |
| elem_bytes = 1 |
| else: |
| |
| elem_bytes = torch.empty(0, dtype=dtype).element_size() |
|
|
| M_shard = M // world_size |
|
|
| |
| mem_input = M * N * elem_bytes |
|
|
| |
| mem_rs = M_shard * N * elem_bytes |
|
|
| |
| mem_quant = 0 |
| if quant_mode == "fp8_per_token": |
| mem_quant = M_shard * N |
| elif quant_mode == "fp4_per_token": |
| if N % 32 != 0: |
| raise ValueError("FP4 quantization requires N divisible by 32") |
| mem_quant = M_shard * (N // 2) |
|
|
| |
| mem_gather = 0 |
| if all_gather: |
| if quant_mode == "fp8_per_token": |
| mem_gather = M * N |
| elif quant_mode == "fp4_per_token": |
| mem_gather = M * (N // 2) |
| else: |
| mem_gather = M * N * elem_bytes |
|
|
| |
| total_bytes = mem_input + mem_rs + mem_quant + mem_gather |
| total_with_overhead = int(math.ceil(total_bytes * overhead_factor)) |
|
|
| logger.debug( |
| f"Heap size calculation: M={M}, N={N}, dtype={dtype}, world_size={world_size}, " |
| f"quant_mode={quant_mode}, all_gather={all_gather}\n" |
| f" Input: {mem_input:,} bytes\n" |
| f" RS buffer: {mem_rs:,} bytes\n" |
| f" Quant buffer: {mem_quant:,} bytes\n" |
| f" Gather buffer: {mem_gather:,} bytes\n" |
| f" Total (with {overhead_factor}x overhead): {total_with_overhead:,} bytes " |
| f"({total_with_overhead / (1024**3):.2f} GB)" |
| ) |
|
|
| return total_with_overhead |
|
|
|
|
| class IrisCommContext: |
| """ |
| Minimal context wrapper for Iris-based communication operations. |
| |
| This is a thin wrapper around iris.iris() that provides convenient access |
| to the iris context for use in reduce-scatter and all-gather operations. |
| |
| Example: |
| >>> # Manual heap size |
| >>> with IrisCommContext(heap_size=2**30) as ctx: |
| >>> shard = ctx.iris_ctx.zeros((1024, 1024), dtype=torch.float32) |
| >>> full = all_gather(shard, ctx) |
| |
| >>> # Automatic heap size calculation |
| >>> heap_size = calculate_heap_size(4096, 4096, torch.float32, |
| ... world_size=2, quant_mode="fp8_per_token") |
| >>> with IrisCommContext(heap_size=heap_size) as ctx: |
| >>> # Guaranteed to have enough memory for your operations |
| >>> input_tensor = ctx.iris_ctx.empty((4096, 4096), dtype=torch.float32) |
| """ |
|
|
| def __init__(self, heap_size: int = 1 << 30): |
| """ |
| Initialize Iris communication context. |
| |
| Args: |
| heap_size (int): Size of the symmetric heap in bytes. Default: 1GB |
| Use calculate_heap_size() to automatically determine the required size |
| based on your tensor dimensions and operations. |
| |
| Example: |
| >>> # Option 1: Fixed size |
| >>> ctx = IrisCommContext(heap_size=2**30) # 1GB |
| |
| >>> # Option 2: Auto-calculated size |
| >>> M, N = 4096, 4096 |
| >>> heap_size = calculate_heap_size(M, N, torch.float32, |
| ... world_size=2, quant_mode="fp8_per_token") |
| >>> ctx = IrisCommContext(heap_size=heap_size) |
| """ |
| if not IRIS_AVAILABLE: |
| raise RuntimeError("Iris library is not available. Please install iris.") |
|
|
| self.heap_size = heap_size |
| self.iris_ctx = None |
| self._initialized = False |
|
|
| def __enter__(self): |
| """Initialize Iris context when entering context manager.""" |
| if not self._initialized: |
| self.iris_ctx = iris.iris(heap_size=self.heap_size) |
| self._initialized = True |
| self.cur_rank = self.iris_ctx.cur_rank |
| self.num_ranks = self.iris_ctx.num_ranks |
|
|
| logger.info( |
| f"Iris context initialized: rank {self.cur_rank}/{self.num_ranks}, heap_size={self.heap_size}" |
| ) |
| return self |
|
|
| def __exit__(self, exc_type, exc_val, exc_tb): |
| """ |
| Clean up when exiting context manager. |
| |
| Used by Python's with statement (automatically called). |
| |
| Example: |
| with IrisCommContext(...) as ctx: |
| pass |
| # __exit__() is called here automatically |
| """ |
| |
| pass |
|
|
| @property |
| def is_initialized(self) -> bool: |
| """Check if the Iris context has been initialized.""" |
| return self._initialized |
|
|
| def get_heap_bases(self): |
| """Get the heap bases tensor for use in Triton kernels.""" |
| if not self.is_initialized: |
| raise RuntimeError("Iris context not initialized. Use as context manager.") |
| return self.iris_ctx.heap_bases |
|
|