Utilities#
Helper functions for visualization, benchmarking, and profiling.
Visualization#
attn_gym.utils.visualize_attention_scores(query, key, score_mod=None, mask_mod=None, device='cuda', name='attention_scores', path=None, batch_idx=0, head_idx=0, scale=None)
#
Generate and save a visualization of attention scores.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
Tensor
|
Query tensor of shape (batch_size, num_heads, seq_len_q, head_dim). |
required |
key
|
Tensor
|
Key tensor of shape (batch_size, num_heads, seq_len_k, head_dim). |
required |
score_mod
|
Optional[Callable]
|
If this is set this will take precedence over the mask_mod. |
None
|
mask_mod
|
Optional[Callable]
|
The mask_mod function used to create block_mask |
None
|
device
|
str
|
Device to run computations on (default: "cuda"). |
'cuda'
|
name
|
str
|
Base name for the file and title (default: 'attention_scores'). |
'attention_scores'
|
path
|
Path
|
Path to save the visualization. If None, will be saved to the current working directory. |
None
|
batch_idx
|
int
|
Index of the batch to visualize (default: 0). |
0
|
head_idx
|
int
|
Index of the head to visualize (default: 0). |
0
|
scale
|
float
|
Scale factor to apply to the attention scores. If None, will be set to 1 / sqrt(head_dim). |
None
|
Returns:
| Type | Description |
|---|---|
None
|
None |
attn_gym.utils.create_score_mod(query, key, score_mod, mask_mod, device='cuda', _compile=False, scale=None, batch_idx=0, head_idx=0)
#
Benchmarking#
attn_gym.utils.benchmark_cuda_function_in_microseconds(func, *args, **kwargs)
#
Thin wrapper around do_bench_using_profiling
attn_gym.utils.calculate_tflops(batch, heads, seq_q, seq_kv, head_dim, time_ms, sparsity=0.0)
#
Calculate TFLOPs for attention forward pass.
Profiling#
attn_gym.utils.cuda_kernel_profiler(kernel_pattern='flash_attncute')
#
Context manager that profiles CUDA kernels and checks for a pattern.
Usage
with cuda_kernel_profiler("flash_attncute") as result: flex_attention(...) print(result["found"]) # True if flash kernel was called print(result["kernel_names"]) # List of all CUDA kernel names
Distributed#
Copy-engine transfers into symmetric memory, for groups inside one NVLink domain (one node or an NVL72 rack). Payload copies do not occupy SMs, so they overlap attention kernels well; they still share memory and NVLink bandwidth.
attn_gym.distributed.CopyEngineAllGather
#
All-gather into a persistent symmetric buffer, written directly in the consumer's layout.
Each rank contributes num_parts tensors of part_shape (for example K and V). The
output has shape (num_parts, *part_shape) with part_shape[dim] multiplied by the world
size, and rank r's shard of every part lands at offset r * part_shape[dim] along
dim: there is no gathered-then-reordered intermediate. Every rank pushes its shard into
every peer's buffer (one contiguous copy per row in front of dim), so the side stream does
P * num_parts * prod(part_shape[:dim]) copies.
start(parts) issues the pushes; wait(parts) returns the gathered buffer. When any part
requires grad, wait is differentiable: its backward reduce-scatters the gradient over NCCL.
Every start first tells peers this rank's buffer is free, then waits for each peer's free
signal before writing into it. The signal is queued behind everything already on the current
stream (including any backward that reads the previous gather), so one gather may be
outstanding at a time, and every call issues the same signals, so a captured start replays
correctly whether or not the gather ran eagerly first.
reduce_scatter(grad)
#
Sum grad (shaped like the output) over ranks; return this rank's shard per part.
attn_gym.distributed.CopyEngineChannel
#
Double-buffered pushes to dst and receives from src (ranks within group).
Each rank owns two receive slots of shape (num_parts, *part_shape) that src writes
alternately, one per step. A receiver frees a slot only after its compute (and any onward
push from that slot) is queued, so a slot is never overwritten while it is being read.
Per step i: send(parts, i) pushes into the slot dst reads at i + 1,
recv(i) waits for and returns this rank's slot for i, and release(i) hands it
back. Call join() before work that must see every pending push finished (for example the
end of a CUDA graph capture).
attn_gym.distributed.same_nvlink_domain(group=None)
#
True when every rank of group shares one NVLink domain, so copy-engine transfers work.
Collective: every rank of group must call it.
attn_gym.distributed.nvlink_domain()
#
Identify the NVLink domain of the current GPU.
On GPUs attached to an NVLink fabric (GB200/GB300 NVL72 racks) this is the fabric cluster
UUID and clique reported by NVML (needs nvidia-ml-py). Otherwise it falls back to the host
name, which assumes the GPUs of one host are NVLink peers (true on HGX/DGX-style systems, not
on PCIe-only boxes).
Misc#
attn_gym.utils.get_flash_block_size(device='cuda')
#
Get block size for Flash backend based on GPU compute capability.
On SM100+ (Blackwell): Q block must be 256, KV block is 128. On SM80/SM90: Both use default 128.