e2fe33180d
Adds an optional ``cube_start: int = 0`` field to ``DPPolicy``. In ``resolve_dp_policy`` the generated ``ShardSpec.cube`` is now ``policy.cube_start + cube_id`` instead of just ``cube_id``. With the default value (0) every existing call site resolves to identical ``ShardSpec.cube`` values (cubes 0..num_cubes-1). Why this is needed: the GQA Llama-70B 8-KV-group headline target lays two 2x4 KV-groups per SIP — the first on cubes 0..7 (rows 0..1), the second on cubes 8..15 (rows 2..3). Without ``cube_start``, ``DPPolicy`` can only address the first 8 row-major cubes, so a second launch on the same SIP overlaps the first. ``cube_start=8`` selects the second 2x4 sub-mesh directly. Design choice: scalar ``cube_start`` (vs a 2D ``cube_mesh_origin`` or arbitrary ``cube_ids`` list) was picked because (a) the 2D mesh-mlo kernel already assumes row-major contiguous cubes, (b) it pairs naturally with ``num_cubes`` (range = ``[start, start+count)``), and (c) zero migration churn — every existing call site stays unchanged. Scope: 5 production LOC (one field + one expression in resolve). No kernel changes here; kernels that consume ``program_id(axis=1)`` for ring arithmetic need their own follow-up (see ADR-0022 contract). Tests: 7 new unit tests in ``test_dppolicy_cube_start.py`` covering default behavior preservation (cube_start=0), shifted ranges (cube_start=8 → cubes 8..15), full-SIP CCL pattern, replicate cube policy, shard-count preservation, and uniqueness. ADR-0026 regression suite (12 tests) stays green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
229 lines
7.2 KiB
Python
229 lines
7.2 KiB
Python
"""Data-parallel placement policy (ADR-0026: intra-device only).
|
||
|
||
``DPPolicy`` describes how a tensor is sharded *within a single SIP* across
|
||
that SIP's cubes and PEs. Crossing the SIP boundary is not a DPPolicy
|
||
concern: ADR-0024's ``torch.ahbm.set_device(rank)`` picks the SIP, and
|
||
Megatron-style TP (ADR-0027) expresses multi-SIP tensors when needed.
|
||
|
||
``ShardSpec`` is expressed in structural ``(sip, cube, pe)`` coordinates.
|
||
The former flat ``pe_index`` field/property is fully removed — callers
|
||
needing a flat integer key compute it explicitly at the call site.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from math import ceil
|
||
from typing import Literal
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DPPolicy:
|
||
"""Intra-device (cube × PE) data-parallel policy.
|
||
|
||
SIP-level placement is controlled by ``torch.ahbm.set_device(rank)``
|
||
(ADR-0024). For tensors that must cross SIP boundaries, use
|
||
Megatron-style parallel layers (ADR-0027). DPPolicy itself never
|
||
crosses a SIP boundary.
|
||
|
||
Policies (per axis):
|
||
- "replicate": full copy at each unit
|
||
- "column_wise": split K (column) axis across units
|
||
- "row_wise": split M (row) axis across units
|
||
|
||
Optional overrides (``None`` = use topology dimensions):
|
||
- num_pes: override PEs per cube
|
||
- num_cubes: override cubes per SIP
|
||
|
||
Cube range:
|
||
- cube_start: index of the first cube in the SIP to place shards
|
||
on. Defaults to 0. Enables disjoint sub-meshes within one SIP
|
||
(e.g. cubes 8..15 alongside cubes 0..7) — required by the
|
||
GQA Llama-70B 8-KV-group headline target (two 2×4 KV-groups
|
||
per SIP).
|
||
"""
|
||
|
||
cube: Literal["replicate", "column_wise", "row_wise"] = "replicate"
|
||
pe: Literal["replicate", "column_wise", "row_wise"] = "replicate"
|
||
num_pes: int | None = None
|
||
num_cubes: int | None = None
|
||
cube_start: int = 0
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ShardSpec:
|
||
"""Structural shard placement — ``(sip, cube, pe)`` coord (ADR-0026).
|
||
|
||
Global-flat ``pe_index`` was removed: callers must use structural
|
||
coords directly. If a flat integer key is needed in a local context
|
||
(e.g. internal dict lookup), compute it explicitly at the call site
|
||
and do not expose it in any public API.
|
||
"""
|
||
|
||
sip: int
|
||
cube: int
|
||
pe: int
|
||
offset_bytes: int
|
||
nbytes: int
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class _LocalPeShard:
|
||
"""Internal — PE resolver's return type (ADR-0026 D3).
|
||
|
||
Holds a cube-local PE identifier (``local_pe``) plus the shard's
|
||
byte payload. Lifted into ``ShardSpec`` with full ``(sip, cube, pe)``
|
||
coordinates inside ``resolve_dp_policy``.
|
||
"""
|
||
|
||
local_pe: int
|
||
offset_bytes: int
|
||
nbytes: int
|
||
|
||
|
||
def _split_shape(
|
||
policy: str, shape: tuple[int, int], count: int, itemsize: int,
|
||
) -> list[tuple[tuple[int, int], int]]:
|
||
"""Split shape by policy into (sub_shape, byte_offset) pairs."""
|
||
M, K = shape
|
||
if policy == "replicate":
|
||
return [((M, K), 0)] * count
|
||
elif policy == "column_wise":
|
||
chunk_k = K // count
|
||
return [((M, chunk_k), i * M * chunk_k * itemsize) for i in range(count)]
|
||
elif policy == "row_wise":
|
||
chunk_m = M // count
|
||
return [((chunk_m, K), i * chunk_m * K * itemsize) for i in range(count)]
|
||
else:
|
||
raise ValueError(f"Unknown policy: {policy}")
|
||
|
||
|
||
def resolve_dp_policy(
|
||
policy: DPPolicy,
|
||
*,
|
||
shape: tuple[int, int],
|
||
itemsize: int,
|
||
num_pe: int,
|
||
num_cubes: int = 1,
|
||
target_sip: int,
|
||
) -> list[ShardSpec]:
|
||
"""Resolve a DPPolicy into a list[ShardSpec] on a single SIP.
|
||
|
||
Two-level resolution (cube × PE) within ``target_sip``. Each returned
|
||
``ShardSpec`` carries ``sip=target_sip`` and cube/pe local to the SIP.
|
||
No SIP-level split — DPPolicy is intra-device only (ADR-0026).
|
||
"""
|
||
_PE_RESOLVERS = {
|
||
"replicate": replicate,
|
||
"column_wise": column_wise,
|
||
"row_wise": row_wise,
|
||
}
|
||
resolver = _PE_RESOLVERS.get(policy.pe)
|
||
if resolver is None:
|
||
raise ValueError(f"Unknown pe-level policy: {policy.pe}")
|
||
|
||
all_shards: list[ShardSpec] = []
|
||
|
||
# Level 1: cube within SIP
|
||
cube_splits = _split_shape(policy.cube, shape, num_cubes, itemsize)
|
||
|
||
for cube_id, (cube_shape, cube_offset) in enumerate(cube_splits):
|
||
# Level 2: PE within cube — resolver returns _LocalPeShard
|
||
local_shards = resolver(shape=cube_shape, itemsize=itemsize, num_pe=num_pe)
|
||
|
||
for ls in local_shards:
|
||
all_shards.append(ShardSpec(
|
||
sip=target_sip,
|
||
cube=policy.cube_start + cube_id,
|
||
pe=ls.local_pe,
|
||
offset_bytes=cube_offset + ls.offset_bytes,
|
||
nbytes=ls.nbytes,
|
||
))
|
||
|
||
return all_shards
|
||
|
||
|
||
def column_wise(
|
||
*, shape: tuple[int, int], itemsize: int, num_pe: int,
|
||
) -> list[_LocalPeShard]:
|
||
"""Split K axis into num_pe equal parts. Each PE gets (M, K/P)."""
|
||
M, K = shape
|
||
chunk_k = K // num_pe
|
||
chunk_bytes = M * chunk_k * itemsize
|
||
return [
|
||
_LocalPeShard(local_pe=i, offset_bytes=i * chunk_bytes, nbytes=chunk_bytes)
|
||
for i in range(num_pe)
|
||
]
|
||
|
||
|
||
def row_wise(
|
||
*, shape: tuple[int, int], itemsize: int, num_pe: int,
|
||
) -> list[_LocalPeShard]:
|
||
"""Split M axis into num_pe equal parts. Each PE gets (M/P, K)."""
|
||
M, K = shape
|
||
chunk_m = M // num_pe
|
||
chunk_bytes = chunk_m * K * itemsize
|
||
return [
|
||
_LocalPeShard(local_pe=i, offset_bytes=i * chunk_bytes, nbytes=chunk_bytes)
|
||
for i in range(num_pe)
|
||
]
|
||
|
||
|
||
def replicate(
|
||
*, shape: tuple[int, int], itemsize: int, num_pe: int,
|
||
) -> list[_LocalPeShard]:
|
||
"""Full copy per PE. Each PE gets (M, K)."""
|
||
M, K = shape
|
||
full_bytes = M * K * itemsize
|
||
return [
|
||
_LocalPeShard(local_pe=i, offset_bytes=0, nbytes=full_bytes)
|
||
for i in range(num_pe)
|
||
]
|
||
|
||
|
||
def tiled_column_major(
|
||
*, shape: tuple[int, int], itemsize: int, num_pe: int,
|
||
tile_m: int, tile_k: int,
|
||
) -> list[_LocalPeShard]:
|
||
"""2D tiling, column-major order (K axis first), round-robin across PEs."""
|
||
M, K = shape
|
||
tiles_m = ceil(M / tile_m)
|
||
tiles_k = ceil(K / tile_k)
|
||
tile_bytes = tile_m * tile_k * itemsize
|
||
row_bytes = K * itemsize
|
||
shards: list[_LocalPeShard] = []
|
||
idx = 0
|
||
for mi in range(tiles_m):
|
||
for ki in range(tiles_k):
|
||
offset = (mi * tile_m * row_bytes) + (ki * tile_k * itemsize)
|
||
shards.append(_LocalPeShard(
|
||
local_pe=idx % num_pe,
|
||
offset_bytes=offset,
|
||
nbytes=tile_bytes,
|
||
))
|
||
idx += 1
|
||
return shards
|
||
|
||
|
||
def tiled_row_major(
|
||
*, shape: tuple[int, int], itemsize: int, num_pe: int,
|
||
tile_m: int, tile_k: int,
|
||
) -> list[_LocalPeShard]:
|
||
"""2D tiling, row-major order (M axis first), round-robin across PEs."""
|
||
M, K = shape
|
||
tiles_m = ceil(M / tile_m)
|
||
tiles_k = ceil(K / tile_k)
|
||
tile_bytes = tile_m * tile_k * itemsize
|
||
row_bytes = K * itemsize
|
||
shards: list[_LocalPeShard] = []
|
||
idx = 0
|
||
for ki in range(tiles_k):
|
||
for mi in range(tiles_m):
|
||
offset = (mi * tile_m * row_bytes) + (ki * tile_k * itemsize)
|
||
shards.append(_LocalPeShard(
|
||
local_pe=idx % num_pe,
|
||
offset_bytes=offset,
|
||
nbytes=tile_bytes,
|
||
))
|
||
idx += 1
|
||
return shards
|