File size: 18,684 Bytes
629b123 | 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 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 | # SPDX-FileCopyrightText: © 2025 Tenstorrent USA, Inc.
# SPDX-License-Identifier: Apache-2.0
"""
RMSNorm2D: RMSNorm for 2D mesh topologies (TG, Galaxy).
Execution paths:
- decode_forward - Sharded, Linear topology, cluster_axis=1
- prefill_forward - Interleaved, Linear topology, cluster_axis=1
Key design:
- Both decode and prefill are ALWAYS distributed for 2D mesh
- Uses Linear topology with cluster_axis=1 (gather across columns)
- Decode uses sharded configs, Prefill uses interleaved
- Separate weight for distributed path (sharded across columns, replicated across rows)
"""
from dataclasses import dataclass, replace
from typing import Optional
import ttnn
from models.common.lightweightmodule import LightweightModule
from models.common.modules.lazy_weight import LazyWeight, resolve_lazy_weight
from models.common.modules.tt_ccl import get_tt_ccl
from models.common.tensor_utils import TILE_SIZE
# =============================================================================
# Constants
# =============================================================================
SHARD_HEIGHT = TILE_SIZE
# =============================================================================
# RMSNorm2DConfig
# =============================================================================
@dataclass
class RMSNorm2DConfig:
"""
Configuration for RMSNorm2D - only fields relevant to 2D mesh topologies.
Both paths are always distributed with Linear topology and cluster_axis=1.
Paths:
- Path 4: 2D distributed decode (sharded) - requires decode_* configs
- Path 5: 2D distributed prefill (interleaved) - no program_config needed
Simple usage:
config = RMSNorm2DConfig(weight, cluster_shape=(4, 8))
Override any field:
config = RMSNorm2DConfig(weight, cluster_shape=(4, 8), eps=1e-6)
"""
# Required: weight and cluster_shape
weight: LazyWeight
cluster_shape: tuple[int, int] | None = None # (rows, cols), e.g. (4, 8) for TG
# Normalization settings
eps: float = 1e-5
add_unit_offset: bool = False
# Device and collectives
mesh_device: ttnn.MeshDevice | None = None
tt_ccl: "TT_CCL | None" = None # type: ignore
# Batch size for decode
max_batch_size: int = 32
# Input memory configs (for _load_input_device_tensor_2d)
decode_input_memcfg: ttnn.MemoryConfig | None = None
prefill_input_memcfg: ttnn.MemoryConfig | None = None
# 2D distributed decode configs - sharded
decode_progcfg: ttnn.LayerNormShardedMultiCoreProgramConfig | None = None
decode_stats_memcfg: ttnn.MemoryConfig | None = None
# Compute kernel config (only for prefill - decode uses program_config)
compute_kernel_config_prefill: ttnn.WormholeComputeKernelConfig | None = None
def is_resolved(self) -> bool:
"""Check if all required fields are resolved."""
required = [
"mesh_device",
"cluster_shape",
"tt_ccl",
# Decode sharded configs
"decode_input_memcfg",
"decode_progcfg",
"decode_stats_memcfg",
# Compute config
"compute_kernel_config_prefill",
# Weight (2D always uses distributed/sharded)
"weight",
]
return all(getattr(self, f) is not None for f in required)
# =============================================================================
# RMSNorm2D
# =============================================================================
class RMSNorm2D(LightweightModule):
"""
RMSNorm for 2D mesh topologies (TG, Galaxy).
Both decode and prefill are always distributed with Linear topology.
Execution paths (bound at construction):
- decode_forward -> _decode_2d_distributed (Path 4) - sharded
- prefill_forward -> _prefill_2d_distributed (Path 5) - interleaved
Simple API:
norm = RMSNorm2D(weight)
Power API:
config = RMSNorm2DConfig(weight, cluster_shape=(4, 8), max_batch_size=64)
norm = RMSNorm2D.from_config(config)
"""
def __init__(self, weight: LazyWeight):
"""
Simple API - derives all config from weight.
Args:
weight: RMSNorm weight tensor of shape (dim,) or (1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT)
Note: Use from_config() to customize eps, add_unit_offset, cluster_shape, or other settings.
"""
super().__init__()
self.config = _resolve_2d_config(RMSNorm2DConfig(weight=weight))
self._device_weights_loaded = False
@classmethod
def from_config(cls, config: RMSNorm2DConfig):
"""Power API - any level of customization via config."""
instance = object.__new__(cls)
super(RMSNorm2D, instance).__init__()
instance.config = _resolve_2d_config(config)
instance._device_weights_loaded = False
return instance
def load_device_weights(self):
"""Load weights to device lazily."""
if self._device_weights_loaded:
return
assert self.config.is_resolved(), "config must be resolved before loading device weights!"
# Load weight (sharded across columns, replicated across rows)
self.weight = self.config.weight.get_device_weight()
self._device_weights_loaded = True
def decode_forward(self, x: "ttnn.Tensor | LazyWeight") -> ttnn.Tensor:
"""
2D distributed decode - sharded for TG/Galaxy.
Uses Linear topology, cluster_axis=1, with program_config.
Execution:
to_sharded -> rms_norm_pre_all_gather(sharded) -> all_gather(cluster_axis=1)
-> rms_norm_post_all_gather(sharded)
"""
self.load_device_weights()
x = _load_input_device_tensor_2d(x, self.config, mode="decode")
cfg = self.config
# Convert to sharded memory config
x = ttnn.to_memory_config(x, memory_config=cfg.decode_input_memcfg)
# Run distributed rmsnorm part 1 (sharded)
tt_stats = ttnn.rms_norm_pre_all_gather(x, program_config=cfg.decode_progcfg)
# All gather stats along cluster axis 1 (columns)
cluster_axis = 1
tt_stats = ttnn.experimental.all_gather_async(
tt_stats,
persistent_output_buffer=None,
dim=3,
multi_device_global_semaphore=cfg.tt_ccl.get_and_cycle_ag_semaphore_handles(cluster_axis),
num_links=1,
cluster_axis=cluster_axis,
topology=ttnn.Topology.Linear,
memory_config=cfg.decode_stats_memcfg,
barrier_semaphore=cfg.tt_ccl.get_and_cycle_barrier_semaphore_handle(cluster_axis),
chunks_per_sync=10,
num_workers_per_link=2,
num_buffers_per_channel=2,
)
# Run distributed rmsnorm part 2
tt_out = ttnn.rms_norm_post_all_gather(
x,
epsilon=cfg.eps,
weight=self.weight,
program_config=cfg.decode_progcfg,
stats=tt_stats,
)
tt_stats.deallocate(True)
return tt_out
def prefill_forward(self, x: "ttnn.Tensor | LazyWeight") -> ttnn.Tensor:
"""
2D distributed prefill - interleaved for TG/Galaxy.
Uses Linear topology, cluster_axis=1.
Execution:
rms_norm_pre_all_gather -> reshape -> all_gather(cluster_axis=1)
-> rms_norm_post_all_gather
"""
self.load_device_weights()
x = _load_input_device_tensor_2d(x, self.config, mode="prefill")
cfg = self.config
# Run distributed rmsnorm part 1
tt_stats = ttnn.rms_norm_pre_all_gather(
x, compute_kernel_config=cfg.compute_kernel_config_prefill, dtype=ttnn.bfloat16
)
# Reshape stats for all_gather (preserve batch dimension for multi-batch prefill)
batch = x.shape[0]
padded_shape = (batch, 1, x.shape[-2], 32)
tt_stats = ttnn.reshape(tt_stats, ttnn.Shape(padded_shape))
# All gather stats along cluster axis 1 (columns)
cluster_axis = 1
tt_stats_gathered = ttnn.experimental.all_gather_async(
tt_stats,
persistent_output_buffer=None,
dim=3,
multi_device_global_semaphore=cfg.tt_ccl.get_and_cycle_ag_semaphore_handles(cluster_axis),
num_links=1,
cluster_axis=cluster_axis,
topology=ttnn.Topology.Linear,
memory_config=ttnn.DRAM_MEMORY_CONFIG,
barrier_semaphore=cfg.tt_ccl.get_and_cycle_barrier_semaphore_handle(cluster_axis),
chunks_per_sync=10,
num_workers_per_link=2,
num_buffers_per_channel=2,
)
tt_stats.deallocate(True)
# Run distributed rmsnorm part 2
tt_out = ttnn.rms_norm_post_all_gather(
x,
tt_stats_gathered,
epsilon=cfg.eps,
weight=self.weight,
compute_kernel_config=cfg.compute_kernel_config_prefill,
)
tt_stats_gathered.deallocate(True)
return tt_out
# =========================================================================
# Forward dispatcher
# =========================================================================
def forward(self, x: "ttnn.Tensor | LazyWeight", mode: str) -> ttnn.Tensor:
"""Dispatch to decode_forward or prefill_forward based on mode."""
if mode == "decode":
return self.decode_forward(x)
else:
return self.prefill_forward(x)
# =========================================================================
# Factory method for TTTv1 compatibility
# =========================================================================
@classmethod
def from_model_args(
cls,
mesh_device,
tt_ccl,
args,
state_dict,
weight_cache_path,
layer_num: int,
weight_key: str,
state_dict_prefix: Optional[str] = None,
):
"""Factory method for backward compatibility with ModelArgs."""
# Validate TG topology
cluster_shape = tuple(mesh_device.shape)
valid_shapes = [(4, 8), (8, 4)]
if cluster_shape not in valid_shapes:
raise ValueError(
f"RMSNorm2D requires Galaxy topology (4x8 or 8x4), got {cluster_shape}. "
"Use RMSNorm1D for non-Galaxy devices."
)
# Build weight name from state_dict_prefix and weight_key
if state_dict_prefix:
weight_name = f"{state_dict_prefix}{weight_key}.weight"
else:
if layer_num is None:
weight_name = f"{weight_key}.weight"
else:
weight_name = f"layers.{layer_num}.{weight_key}.weight"
# Transform weight to expected shape: (1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT)
dim = args.dim
torch_weight = (
state_dict[weight_name].unsqueeze(0).view(1, 1, dim).reshape([1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT])
)
# Add offset before creating LazyWeight
if args.rms_norm_add_unit_offset:
torch_weight = torch_weight + 1.0
# Create LazyWeight (mesh_mapper_config is handled by _resolve_2d_config)
lazy_weight = LazyWeight(
source=torch_weight,
dtype=ttnn.bfloat16,
device=mesh_device,
layout=ttnn.ROW_MAJOR_LAYOUT,
memory_config=ttnn.DRAM_MEMORY_CONFIG,
cache_dir_weight_name=(weight_cache_path, weight_name + "_2d") if weight_cache_path else None,
)
# Build config (dim derived from weight in _resolve_2d_config)
config = RMSNorm2DConfig(
weight=lazy_weight,
cluster_shape=cluster_shape,
eps=args.norm_eps,
add_unit_offset=False, # Already applied above
mesh_device=mesh_device,
tt_ccl=tt_ccl,
max_batch_size=args.max_batch_size,
)
return cls.from_config(config)
# =============================================================================
# Config resolution
# =============================================================================
def _resolve_2d_config(config: RMSNorm2DConfig) -> RMSNorm2DConfig:
"""Resolve config defaults for RMSNorm2D."""
to_set = {}
# --- Phase 1: Foundational fields ---
# Derive dim from weight (works for any shape: [dim], [1, dim], [1, 1, dim//32, 32], etc.)
dim = config.weight.source.numel()
# Derive mesh_device from weight
mesh_device = config.mesh_device
if mesh_device is None:
mesh_device = config.weight.device
if mesh_device is None:
mesh_device = ttnn.GetDefaultDevice()
if config.mesh_device is None:
to_set["mesh_device"] = mesh_device
assert mesh_device is not None, "mesh_device must be available!"
# Derive cluster_shape from mesh_device if not provided
cluster_shape = config.cluster_shape
if cluster_shape is None:
cluster_shape = tuple(mesh_device.shape)
to_set["cluster_shape"] = cluster_shape
assert len(cluster_shape) == 2 and all(
d > 1 for d in cluster_shape
), f"cluster_shape must be 2D with both dims > 1, got {cluster_shape}"
num_rows, num_cols = cluster_shape
# Derive tt_ccl (always needed for 2D)
if config.tt_ccl is None:
to_set["tt_ccl"] = get_tt_ccl(mesh_device)
# --- Phase 2: Compute kernel config (prefill only - decode uses program_config) ---
if config.compute_kernel_config_prefill is None:
# 2D uses fp32=False, packer_l1=False (from DistributedNorm.ln_cfg)
to_set["compute_kernel_config_prefill"] = ttnn.WormholeComputeKernelConfig(
math_fidelity=ttnn.MathFidelity.HiFi2,
math_approx_mode=False,
fp32_dest_acc_en=False,
packer_l1_acc=False,
)
# --- Phase 3: 2D distributed decode configs (Path 4) ---
hidden_size_per_device = dim // num_cols
core_grid_ln = (
min(4, hidden_size_per_device // TILE_SIZE // 8),
8,
)
num_cores_ln = core_grid_ln[0] * core_grid_ln[1]
if config.decode_input_memcfg is None:
to_set["decode_input_memcfg"] = ttnn.create_sharded_memory_config(
shape=(1, 1, 32, hidden_size_per_device),
core_grid=ttnn.CoreGrid(y=core_grid_ln[0], x=core_grid_ln[1]),
strategy=ttnn.ShardStrategy.WIDTH,
)
if config.decode_progcfg is None:
to_set["decode_progcfg"] = ttnn.LayerNormShardedMultiCoreProgramConfig(
compute_with_storage_grid_size=(core_grid_ln[1], core_grid_ln[0]),
subblock_w=(hidden_size_per_device // num_cores_ln) // TILE_SIZE,
block_h=1,
block_w=(hidden_size_per_device // num_cores_ln) // TILE_SIZE,
inplace=False,
)
if config.decode_stats_memcfg is None:
# Stats memory: 32 x (32 * num_cols) where num_cols is the cluster width
to_set["decode_stats_memcfg"] = ttnn.create_sharded_memory_config(
shape=[1, 1, 32, 32 * num_cols],
core_grid=ttnn.CoreGrid(y=1, x=1),
strategy=ttnn.ShardStrategy.WIDTH,
)
# --- Phase 4: Resolve weight (sharded across columns, replicated across rows) ---
# 2D always uses distributed paths
# Weight shape must be (1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT), shard on dim 2
# Reshape weight to expected shape if needed (handles [dim], [1, dim], etc.)
assert dim % SHARD_HEIGHT == 0, f"dim must be divisible by SHARD_HEIGHT={SHARD_HEIGHT}, got {dim}"
expected_shape = (1, 1, dim // SHARD_HEIGHT, SHARD_HEIGHT)
if config.weight.source.shape != expected_shape:
transformed_source = config.weight.source.reshape(*expected_shape)
else:
transformed_source = config.weight.source
# Apply unit offset if requested (e.g., for Gemma models)
if config.add_unit_offset:
transformed_source = transformed_source + 1.0
# Create a new LazyWeight with the transformed source
transformed_weight = replace(config.weight, source=transformed_source)
# Shard on tensor dim 2 across columns (cluster axis 1), replicate across rows (cluster axis 0)
mesh_mapper_config = ttnn.MeshMapperConfig(
placements=[ttnn.PlacementReplicate(), ttnn.PlacementShard(2)],
mesh_shape_override=ttnn.MeshShape(list(cluster_shape)),
)
resolved_weight = resolve_lazy_weight(
transformed_weight,
device=mesh_device,
layout=ttnn.ROW_MAJOR_LAYOUT,
memory_config=ttnn.DRAM_MEMORY_CONFIG,
mesh_mapper_config=mesh_mapper_config,
)
to_set["weight"] = resolved_weight
return replace(config, **to_set)
# =============================================================================
# Input Tensor Utilities
# =============================================================================
def _load_input_device_tensor_2d(x: "ttnn.Tensor | LazyWeight", config: RMSNorm2DConfig, mode: str) -> ttnn.Tensor:
"""
Resolve the input tensor to ttnn tensor if x is a LazyWeight, otherwise return as is.
For 2D distributed, shards input on last dim across columns (cluster_axis=1).
Args:
x: Input tensor (ttnn.Tensor or LazyWeight wrapping torch tensor)
config: Resolved RMSNorm2DConfig
mode: "decode" or "prefill"
Returns:
ttnn.Tensor ready for the actual forward computation
"""
assert mode in ("decode", "prefill"), f"mode must be 'decode' or 'prefill', got {mode}"
if isinstance(x, LazyWeight):
num_rows, num_cols = config.cluster_shape
# Determine memory config based on mode
if mode == "decode":
mem_cfg = config.decode_input_memcfg or ttnn.DRAM_MEMORY_CONFIG
else:
mem_cfg = config.prefill_input_memcfg or ttnn.DRAM_MEMORY_CONFIG
# Shard input on last dim across columns, replicate across rows
mesh_mapper_config = ttnn.MeshMapperConfig(
placements=[ttnn.PlacementReplicate(), ttnn.PlacementShard(-1)],
mesh_shape_override=ttnn.MeshShape(num_rows, num_cols),
)
resolved_x = resolve_lazy_weight(
x,
device=config.mesh_device,
memory_config=mem_cfg,
mesh_mapper_config=mesh_mapper_config,
layout=ttnn.TILE_LAYOUT,
)
return resolved_x.get_device_weight()
# Already a ttnn.Tensor - return as is
assert isinstance(x, ttnn.Tensor), f"x must be ttnn.Tensor or LazyWeight, got {type(x)}"
return x
|