File size: 9,148 Bytes
5dc80b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | """
Triton kernels for HRM SRAM/DRAM memory-tiered operations.
Key idea: In Triton, SRAM = registers + shared memory (managed by compiler
within a block). DRAM = global memory (GPU HBM). By structuring kernels to keep
L-level state tile-resident (loaded once, reused many times within a block),
we ensure L-level stays in SRAM. H-level state is loaded from global memory
(DRAM) each cycle, paying the full memory bandwidth cost.
This gives us real, measurable latency differences that map to the HRM's
hierarchical update frequencies.
"""
import torch
import triton
import triton.language as tl
import math
# ---------------------------------------------------------------------------
# 1. SRAM-Resident Fused RMS-Norm + Residual (for L-level, keeps state in regs)
# ---------------------------------------------------------------------------
@triton.jit
def _rms_norm_residual_fused_kernel(
X_ptr, # Input tensor (residual branch)
Residual_ptr, # Residual connection input
Out_ptr, # Output tensor
N: tl.constexpr, # Hidden dimension (constexpr → compiler tiles in SRAM)
eps: tl.constexpr,
BLOCK_N: tl.constexpr,
):
"""Fused RMS-norm + residual add.
By making N and BLOCK_N constexpr, the compiler keeps the entire hidden
vector in registers/shared-memory (SRAM) across the norm computation.
This is the kernel used for L-level (fast path).
"""
row = tl.program_id(0)
cols = tl.arange(0, BLOCK_N)
mask = cols < N
# ---- Load both inputs into SRAM (registers) in one shot ----
x = tl.load(X_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
r = tl.load(Residual_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
# Residual add — stays in registers
h = x + r
# RMS norm — entirely in registers, no global memory round-trip
variance = tl.sum(h * h, axis=0) / N
h_norm = h * tl.math.rsqrt(variance + eps)
# ---- Store back to global memory ----
tl.store(Out_ptr + row * N + cols, h_norm.to(tl.bfloat16), mask=mask)
# ---------------------------------------------------------------------------
# 2. DRAM-Sourced RMS-Norm + Residual (for H-level, explicit global loads)
# ---------------------------------------------------------------------------
@triton.jit
def _rms_norm_residual_dram_kernel(
X_ptr,
Residual_ptr,
Out_ptr,
N: tl.constexpr,
eps: tl.constexpr,
BLOCK_N: tl.constexpr,
):
"""RMS-norm + residual for H-level.
Structurally identical but designed to be called with larger strides
and without re-use inside a meta-kernel. Each call does a full
DRAM round-trip, modeling the slower H-level memory access pattern.
"""
row = tl.program_id(0)
cols = tl.arange(0, BLOCK_N)
mask = cols < N
# Global memory load (DRAM)
x = tl.load(X_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
r = tl.load(Residual_ptr + row * N + cols, mask=mask, other=0.0).to(tl.float32)
h = x + r
variance = tl.sum(h * h, axis=0) / N
h_norm = h * tl.math.rsqrt(variance + eps)
tl.store(Out_ptr + row * N + cols, h_norm.to(tl.bfloat16), mask=mask)
# ---------------------------------------------------------------------------
# 3. SRAM-Resident Fused SwiGLU (for L-level, keeps activations in SRAM)
# ---------------------------------------------------------------------------
@triton.jit
def _swiglu_fused_sram_kernel(
GateUp_ptr, # [rows, 2 * inter] — gate and up projections concatenated
Out_ptr, # [rows, inter]
inter: tl.constexpr,
BLOCK_INTER: tl.constexpr,
):
"""Fused SiLU(gate) * up in a single kernel pass.
For L-level: The gate and up vectors are loaded once into registers
and the activation is computed without spilling to DRAM.
"""
row = tl.program_id(0)
cols = tl.arange(0, BLOCK_INTER)
mask = cols < inter
# Load gate and up from contiguous memory — both go into SRAM
gate = tl.load(GateUp_ptr + row * 2 * inter + cols, mask=mask, other=0.0).to(tl.float32)
up = tl.load(GateUp_ptr + row * 2 * inter + inter + cols, mask=mask, other=0.0).to(tl.float32)
# SiLU(gate) * up — entirely in registers
silu_gate = gate * tl.sigmoid(gate)
result = silu_gate * up
tl.store(Out_ptr + row * inter + cols, result.to(tl.bfloat16), mask=mask)
# ---------------------------------------------------------------------------
# 4. State Transfer Kernel: SRAM-tier ↔ DRAM-tier (H↔L communication)
# ---------------------------------------------------------------------------
@triton.jit
def _state_transfer_kernel(
Src_ptr,
Dst_ptr,
numel: tl.constexpr,
BLOCK: tl.constexpr,
):
"""Explicit memory copy kernel for cross-tier state transfer.
Used when H-level needs to read L-level output (or vice versa).
Triton compiles this into optimized async memcpy instructions.
"""
pid = tl.program_id(0)
offsets = pid * BLOCK + tl.arange(0, BLOCK)
mask = offsets < numel
data = tl.load(Src_ptr + offsets, mask=mask, other=0.0)
tl.store(Dst_ptr + offsets, data, mask=mask)
# ---------------------------------------------------------------------------
# 5. Benchmarking Kernel: Memory Latency Probe
# ---------------------------------------------------------------------------
@triton.jit
def _memory_latency_probe_kernel(
Data_ptr,
Out_ptr,
N: tl.constexpr,
BLOCK_N: tl.constexpr,
NUM_ITERS: tl.constexpr,
):
"""Probe kernel to measure effective memory latency.
Performs NUM_ITERS dependent loads to measure true SRAM vs DRAM latency.
The data dependency chain prevents compiler reordering.
"""
pid = tl.program_id(0)
cols = tl.arange(0, BLOCK_N)
mask = cols < N
# Initial load from global memory
acc = tl.load(Data_ptr + pid * N + cols, mask=mask, other=0.0)
# Dependent iteration chain — forces sequential memory access
for _ in range(NUM_ITERS):
# This stays in SRAM (registers) because acc is reused
acc = acc * 1.00001 + 0.00001
tl.store(Out_ptr + pid * N + cols, acc, mask=mask)
# ===================================================================
# Python wrappers
# ===================================================================
def _next_power_of_2(n: int) -> int:
return 1 << (n - 1).bit_length()
def triton_rms_norm_residual_sram(
x: torch.Tensor,
residual: torch.Tensor,
eps: float = 1e-5,
) -> torch.Tensor:
"""SRAM-optimized fused RMS-norm + residual for L-level."""
assert x.shape == residual.shape
assert x.is_contiguous() and residual.is_contiguous()
rows, N = x.shape[0] * (x.shape[1] if x.ndim == 3 else 1), x.shape[-1]
flat_x = x.reshape(rows, N)
flat_r = residual.reshape(rows, N)
out = torch.empty_like(flat_x)
BLOCK_N = _next_power_of_2(N)
_rms_norm_residual_fused_kernel[(rows,)](
flat_x, flat_r, out,
N=N, eps=eps, BLOCK_N=BLOCK_N,
)
return out.reshape(x.shape)
def triton_rms_norm_residual_dram(
x: torch.Tensor,
residual: torch.Tensor,
eps: float = 1e-5,
) -> torch.Tensor:
"""DRAM-path RMS-norm + residual for H-level."""
assert x.shape == residual.shape
assert x.is_contiguous() and residual.is_contiguous()
rows, N = x.shape[0] * (x.shape[1] if x.ndim == 3 else 1), x.shape[-1]
flat_x = x.reshape(rows, N)
flat_r = residual.reshape(rows, N)
out = torch.empty_like(flat_x)
BLOCK_N = _next_power_of_2(N)
_rms_norm_residual_dram_kernel[(rows,)](
flat_x, flat_r, out,
N=N, eps=eps, BLOCK_N=BLOCK_N,
)
return out.reshape(x.shape)
def triton_swiglu_sram(gate_up: torch.Tensor, inter: int) -> torch.Tensor:
"""SRAM-optimized fused SwiGLU for L-level."""
assert gate_up.is_contiguous()
rows = gate_up.shape[0] * (gate_up.shape[1] if gate_up.ndim == 3 else 1)
flat = gate_up.reshape(rows, -1)
out = torch.empty(rows, inter, dtype=gate_up.dtype, device=gate_up.device)
BLOCK_INTER = _next_power_of_2(inter)
_swiglu_fused_sram_kernel[(rows,)](
flat, out,
inter=inter, BLOCK_INTER=BLOCK_INTER,
)
return out.reshape(*gate_up.shape[:-1], inter)
def triton_state_transfer(src: torch.Tensor, dst: torch.Tensor) -> None:
"""Explicit cross-tier state copy (H↔L communication)."""
assert src.is_contiguous() and dst.is_contiguous()
assert src.numel() == dst.numel()
numel = src.numel()
BLOCK = 1024
grid = ((numel + BLOCK - 1) // BLOCK,)
_state_transfer_kernel[grid](
src, dst,
numel=numel, BLOCK=BLOCK,
)
def triton_memory_latency_probe(
data: torch.Tensor,
num_iters: int = 100,
) -> torch.Tensor:
"""Measure effective memory latency via dependent load chain."""
assert data.is_contiguous()
rows, N = data.shape[0], data.shape[-1]
flat = data.reshape(rows, N)
out = torch.empty_like(flat)
BLOCK_N = _next_power_of_2(N)
_memory_latency_probe_kernel[(rows,)](
flat, out,
N=N, BLOCK_N=BLOCK_N, NUM_ITERS=num_iters,
)
return out
|