priml 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- priml/__init__.py +1 -0
- priml/compile.py +131 -0
- priml/custom_types.py +80 -0
- priml/data/__init__.py +3 -0
- priml/data/custom_types.py +39 -0
- priml/data/dummy.py +98 -0
- priml/distributed/README.md +39 -0
- priml/distributed/testing.py +391 -0
- priml/hub.py +199 -0
- priml/image.py +386 -0
- priml/inference/__init__.py +4 -0
- priml/launch.py +382 -0
- priml/lib/__init__.py +1 -0
- priml/lib/custom_json.py +742 -0
- priml/lib/custom_types.py +65 -0
- priml/lib/testing/__init__.py +3 -0
- priml/lib/testing/main.py +39 -0
- priml/lib/traverse.py +235 -0
- priml/lib/userdirs.py +178 -0
- priml/logger.py +219 -0
- priml/loss/__init__.py +25 -0
- priml/loss/cross_entropy.py +74 -0
- priml/loss/custom_types.py +60 -0
- priml/loss/diffusion.py +148 -0
- priml/loss/gan.py +83 -0
- priml/loss/lpips_loss.py +80 -0
- priml/loss/simple_loss.py +85 -0
- priml/loss/stablemax.py +64 -0
- priml/loss/weighted_loss.py +91 -0
- priml/math/__init__.py +3 -0
- priml/math/basic.py +161 -0
- priml/math/custom_types.py +207 -0
- priml/math/diffusion/__init__.py +78 -0
- priml/math/diffusion/sampling.py +470 -0
- priml/math/diffusion/schedule.py +528 -0
- priml/math/diffusion/target.py +435 -0
- priml/math/distributed.py +78 -0
- priml/math/frequency.py +173 -0
- priml/math/numeric.py +878 -0
- priml/math/pixel.py +500 -0
- priml/math/pooling.py +231 -0
- priml/math/probability.py +655 -0
- priml/math/seed.py +397 -0
- priml/math/stats.py +408 -0
- priml/math/tensor.py +165 -0
- priml/metrics/__init__.py +14 -0
- priml/metrics/binary_accuracy.py +82 -0
- priml/metrics/custom_types.py +50 -0
- priml/metrics/topk.py +98 -0
- priml/model/__init__.py +96 -0
- priml/model/attention.py +795 -0
- priml/model/causal_lm.py +171 -0
- priml/model/conv.py +259 -0
- priml/model/custom_types.py +90 -0
- priml/model/embedding.py +53 -0
- priml/model/gated_delta_net.py +261 -0
- priml/model/generate.py +188 -0
- priml/model/goldens/gated_delta_net_min_cpu.pt +0 -0
- priml/model/goldens/mla_min_cpu.pt +0 -0
- priml/model/goldens/mmdit_block_min_cpu.pt +0 -0
- priml/model/goldens/moe_min_cpu.pt +0 -0
- priml/model/goldens/transformer_block_min_cpu.pt +0 -0
- priml/model/init.py +84 -0
- priml/model/kimi_k2.py +400 -0
- priml/model/kvcache.py +106 -0
- priml/model/linear.py +196 -0
- priml/model/mla.py +516 -0
- priml/model/mlpmixer.py +102 -0
- priml/model/mmdit.py +246 -0
- priml/model/moe.py +390 -0
- priml/model/norm.py +306 -0
- priml/model/patchify.py +116 -0
- priml/model/qwen3.py +257 -0
- priml/model/rope.py +587 -0
- priml/model/sequential.py +113 -0
- priml/model/special.py +60 -0
- priml/model/swiglu.py +211 -0
- priml/model/transformer.py +140 -0
- priml/model/whitening.py +61 -0
- priml/optimizers/__init__.py +137 -0
- priml/optimizers/adam_atan2.py +104 -0
- priml/optimizers/muon.py +143 -0
- priml/optimizers/newton.py +201 -0
- priml/optimizers/sign_sgd.py +309 -0
- priml/runtime.py +519 -0
- priml/sharding.py +74 -0
- priml/testing/__init__.py +1 -0
- priml/testing/bfb.py +794 -0
- priml/testing/fixtures.py +48 -0
- priml/train/__init__.py +70 -0
- priml/train/activation.py +514 -0
- priml/train/checkpointing.py +756 -0
- priml/train/custom_types.py +501 -0
- priml/train/docs/mesh_native_design.md +239 -0
- priml/train/ema.py +499 -0
- priml/train/grad_clip.py +114 -0
- priml/train/learnable.py +302 -0
- priml/train/parallelism.py +485 -0
- priml/train/profiling.py +537 -0
- priml/train/progress.py +61 -0
- priml/train/quantization.py +176 -0
- priml/train/results.py +96 -0
- priml/train/schedules.py +95 -0
- priml/train/tensor_parallel.py +200 -0
- priml/train/tracker.py +545 -0
- priml/train/train_loop.py +1349 -0
- priml/train/train_step.py +321 -0
- priml/train/train_step_gan.py +284 -0
- priml-0.1.0.dist-info/METADATA +78 -0
- priml-0.1.0.dist-info/RECORD +112 -0
- priml-0.1.0.dist-info/WHEEL +4 -0
- priml-0.1.0.dist-info/licenses/LICENSE +201 -0
priml/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""priml — malleable ML building blocks for training experiments."""
|
priml/compile.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Utilities for torch.compile diagnostics and lazy-compilation decorators.
|
|
2
|
+
|
|
3
|
+
Module-level ``@torch.compile`` and ``@torch.compiler.assume_constant_result``
|
|
4
|
+
load ~400 torch modules (dynamo, inductor, functorch, functorch's symbolic
|
|
5
|
+
shapes, ...) just to construct the lazy trampoline — about 1s each. ``lazy_compile``
|
|
6
|
+
and ``lazy_assume_constant_result`` defer that construction to first call, so
|
|
7
|
+
modules that decorate but never run in a given process pay no import-time cost.
|
|
8
|
+
First call is slower by the deferred amount; subsequent calls are identical to
|
|
9
|
+
the non-lazy version.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import traceback
|
|
19
|
+
|
|
20
|
+
import torch
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_compile_traces = dict[str, list[str]]()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def lazy_torch_compile(*compile_args: Any, **compile_kwargs: Any) -> Callable[..., Any]:
|
|
27
|
+
"""Lazy ``@torch.compile`` — defers dynamo/inductor imports to first call.
|
|
28
|
+
|
|
29
|
+
Mirrors ``torch.compile``'s dual calling convention: use bare
|
|
30
|
+
(``@lazy_torch_compile``) or parameterized
|
|
31
|
+
(``@lazy_torch_compile(fullgraph=True)``). Parameterized arguments
|
|
32
|
+
(``fullgraph``, ``dynamic``, ``mode``, ``backend``, ``options``, etc.)
|
|
33
|
+
are forwarded verbatim on first invocation; see ``help(torch.compile)``
|
|
34
|
+
for the full reference.
|
|
35
|
+
|
|
36
|
+
Module-level ``@torch.compile`` forces ~400 torch internals
|
|
37
|
+
(dynamo, inductor, functorch, ...) to load at import time just
|
|
38
|
+
to construct the trampoline. This wrapper defers that to first
|
|
39
|
+
call, so processes that import but never invoke pay zero cost.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
result: The lazily-compiled function when applied bare, otherwise a
|
|
43
|
+
decorator awaiting the function to compile.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
# Bare ``@lazy_torch_compile``: the lone positional is the decorated
|
|
47
|
+
# function, not a ``torch.compile`` argument -- decorate it directly.
|
|
48
|
+
if len(compile_args) == 1 and not compile_kwargs and callable(compile_args[0]):
|
|
49
|
+
return _make_lazy_compiled(compile_args[0])
|
|
50
|
+
|
|
51
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
52
|
+
return _make_lazy_compiled(fn, *compile_args, **compile_kwargs)
|
|
53
|
+
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _make_lazy_compiled(
|
|
58
|
+
fn: Callable[..., Any], *compile_args: Any, **compile_kwargs: Any
|
|
59
|
+
) -> Callable[..., Any]:
|
|
60
|
+
"""Wrap ``fn`` so ``torch.compile`` runs on first call, not at decoration."""
|
|
61
|
+
compiled: Callable[..., Any] | None = None
|
|
62
|
+
|
|
63
|
+
@functools.wraps(fn)
|
|
64
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
65
|
+
nonlocal compiled
|
|
66
|
+
target = compiled
|
|
67
|
+
if target is None:
|
|
68
|
+
target = torch.compile(*compile_args, **compile_kwargs)(fn)
|
|
69
|
+
compiled = target
|
|
70
|
+
return target(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def lazy_assume_constant_result(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
76
|
+
"""Lazy ``@torch.compiler.assume_constant_result`` — defers dynamo imports to first call.
|
|
77
|
+
|
|
78
|
+
Same semantics as ``torch.compiler.assume_constant_result``; see
|
|
79
|
+
its docs for what the wrapping buys you inside a compiled region.
|
|
80
|
+
This version pays the dynamo import cost on first call rather
|
|
81
|
+
than at module load.
|
|
82
|
+
"""
|
|
83
|
+
wrapped: Callable[..., Any] | None = None
|
|
84
|
+
|
|
85
|
+
@functools.wraps(fn)
|
|
86
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
87
|
+
nonlocal wrapped
|
|
88
|
+
target = wrapped
|
|
89
|
+
if target is None:
|
|
90
|
+
target = torch.compiler.assume_constant_result(fn)
|
|
91
|
+
wrapped = target
|
|
92
|
+
return target(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@lazy_assume_constant_result
|
|
98
|
+
def trace_compile(
|
|
99
|
+
key: str,
|
|
100
|
+
*,
|
|
101
|
+
max_compiles: int = -1,
|
|
102
|
+
always_print: bool = False,
|
|
103
|
+
) -> int:
|
|
104
|
+
"""Track and optionally limit recompilations. Safe to call from compiled code.
|
|
105
|
+
|
|
106
|
+
Call this inside a compiled function to record each (re)compilation.
|
|
107
|
+
When ``max_compiles`` is exceeded, raises ``RuntimeError`` with all
|
|
108
|
+
collected stack traces so you can diagnose guard failures.
|
|
109
|
+
|
|
110
|
+
Enable verbose torch recompilation logging with::
|
|
111
|
+
|
|
112
|
+
TORCH_LOGS="recompiles_verbose" python script.py
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
key: Identifier for this compilation site.
|
|
116
|
+
max_compiles: Raise after this many compiles (-1 = unlimited).
|
|
117
|
+
always_print: Print the stack trace on every compile.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Number of compiles seen so far for this key.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
trace = "".join(traceback.format_stack()[:-1])
|
|
124
|
+
traces = _compile_traces.setdefault(key, [])
|
|
125
|
+
traces.append(trace)
|
|
126
|
+
if always_print:
|
|
127
|
+
print(trace) # noqa: T201
|
|
128
|
+
if max_compiles > -1 and len(traces) > max_compiles:
|
|
129
|
+
traces_str = "" if always_print else ("\n" + "\n--------\n".join(traces))
|
|
130
|
+
raise RuntimeError(f"Too many compiles ({len(traces)}) for {key}.{traces_str}")
|
|
131
|
+
return len(traces)
|
priml/custom_types.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Torch-free ML types shared across priml.
|
|
2
|
+
|
|
3
|
+
Importable without dragging in torch + jaxtyping + numpy. Tensor type
|
|
4
|
+
aliases and ``convert_to_tensor`` live in
|
|
5
|
+
:mod:`priml.math.custom_types`; channel Protocols live in
|
|
6
|
+
:mod:`priml.model.custom_types`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CheckpointableProtocol",
|
|
19
|
+
"HasNormalizedWorkingDirPattern",
|
|
20
|
+
"Matrix",
|
|
21
|
+
"MetricObjective",
|
|
22
|
+
"Vector",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class HasNormalizedWorkingDirPattern(Protocol):
|
|
28
|
+
"""A path-owning Config that inherits its location from its owner.
|
|
29
|
+
|
|
30
|
+
The structural contract behind parent-to-child path propagation: a parent
|
|
31
|
+
fills a child's ``base_dir`` (only when ``None``) with its own resolved
|
|
32
|
+
``working_dir``, and the child resolves ``working_dir`` beneath it (see
|
|
33
|
+
``priml.lib.userdirs.resolve_working_dir``). ``base_dir`` is inherited
|
|
34
|
+
plumbing; ``working_dir`` is the Config's own opinionated logical location.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
base_dir: Path | str | None
|
|
38
|
+
working_dir: Path | str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Type aliases for embedding and score data structures.
|
|
42
|
+
Vector = Sequence[float]
|
|
43
|
+
"""1D float sequence (embeddings, scores)."""
|
|
44
|
+
|
|
45
|
+
Matrix = Sequence[Vector]
|
|
46
|
+
"""2D float sequence (batch of embeddings/scores)."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
50
|
+
class MetricObjective:
|
|
51
|
+
"""The primary metric a scored run optimizes: its key and direction.
|
|
52
|
+
|
|
53
|
+
``metric_key`` is the full tracker key (e.g. ``"eval/total_loss"``) so a
|
|
54
|
+
consumer never guesses the prefix; ``direction`` says whether lower or higher
|
|
55
|
+
is better. Ranking (leaderboards) and "did it beat the bar" checks need these
|
|
56
|
+
as DATA, not MANIFEST prose.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
metric_key: str
|
|
60
|
+
"""Full tracker metric key, e.g. ``"eval/total_loss"`` or ``"eval/roc_auc"``."""
|
|
61
|
+
|
|
62
|
+
direction: Literal["minimize", "maximize"]
|
|
63
|
+
"""Whether a lower (minimize) or higher (maximize) value is better."""
|
|
64
|
+
|
|
65
|
+
def is_better(self, candidate: float, incumbent: float) -> bool:
|
|
66
|
+
"""Whether ``candidate`` beats ``incumbent`` under this direction."""
|
|
67
|
+
if self.direction == "minimize":
|
|
68
|
+
return candidate < incumbent
|
|
69
|
+
return candidate > incumbent
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@runtime_checkable
|
|
73
|
+
class CheckpointableProtocol(Protocol):
|
|
74
|
+
def state_dict(self) -> dict[str, Any]:
|
|
75
|
+
"""Get state for checkpointing."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def load_state_dict(self, state_dict: dict[str, Any]) -> None:
|
|
79
|
+
"""Load state from checkpoint."""
|
|
80
|
+
...
|
priml/data/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Custom types for data module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from priml.custom_types import CheckpointableProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DatasetProtocol",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class DatasetProtocol(CheckpointableProtocol, Protocol):
|
|
17
|
+
"""Protocol for datasets with train/eval dataloaders.
|
|
18
|
+
|
|
19
|
+
Extends CheckpointableProtocol to support resuming from checkpoints
|
|
20
|
+
(iterator position, shuffling state, etc.).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def train_dataloader(self) -> Any:
|
|
24
|
+
"""Get training dataloader.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
dataloader: Iterator yielding batches (typically dict[str, Tensor]).
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def eval_dataloader(self) -> Any:
|
|
33
|
+
"""Get evaluation dataloader.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
dataloader: Iterator yielding batches (typically dict[str, Tensor]).
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
...
|
priml/data/dummy.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Dummy dataset for testing and default configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from configgle import Fig
|
|
8
|
+
from torch import Tensor
|
|
9
|
+
from torch.utils.data import DataLoader, TensorDataset
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DummyDataset:
|
|
15
|
+
"""Dummy dataset returning random tensors.
|
|
16
|
+
|
|
17
|
+
Useful for testing and default configuration.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
class Config(Fig["DummyDataset"]):
|
|
21
|
+
"""Dummy dataset configuration."""
|
|
22
|
+
|
|
23
|
+
num_samples: int = 100
|
|
24
|
+
batch_size: int = 32
|
|
25
|
+
input_shape: tuple[int, ...] = (3, 224, 224)
|
|
26
|
+
num_classes: int = 1000
|
|
27
|
+
device: str = "cuda"
|
|
28
|
+
seed: int = 0
|
|
29
|
+
"""Seed for the random data/labels so runs are reproducible."""
|
|
30
|
+
num_workers: int = 0
|
|
31
|
+
"""DataLoader worker processes."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config: Config) -> None:
|
|
34
|
+
"""Initialize dummy dataset.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: Dataset configuration.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
self.config = config
|
|
41
|
+
|
|
42
|
+
# Seed a dedicated generator so the synthetic data is reproducible and
|
|
43
|
+
# independent of the global torch RNG state.
|
|
44
|
+
generator = torch.Generator().manual_seed(config.seed)
|
|
45
|
+
data = torch.randn(config.num_samples, *config.input_shape, generator=generator)
|
|
46
|
+
labels = torch.randint(
|
|
47
|
+
0,
|
|
48
|
+
config.num_classes,
|
|
49
|
+
(config.num_samples,),
|
|
50
|
+
generator=generator,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.dataset = TensorDataset(data, labels)
|
|
54
|
+
|
|
55
|
+
def train_dataloader(self) -> DataLoader[Any]:
|
|
56
|
+
"""Get training dataloader."""
|
|
57
|
+
return DataLoader(
|
|
58
|
+
self.dataset,
|
|
59
|
+
batch_size=self.config.batch_size,
|
|
60
|
+
shuffle=True,
|
|
61
|
+
num_workers=self.config.num_workers,
|
|
62
|
+
collate_fn=self._collate_fn,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def eval_dataloader(self) -> DataLoader[Any]:
|
|
66
|
+
"""Get evaluation dataloader."""
|
|
67
|
+
return DataLoader(
|
|
68
|
+
self.dataset,
|
|
69
|
+
batch_size=self.config.batch_size,
|
|
70
|
+
shuffle=False,
|
|
71
|
+
num_workers=self.config.num_workers,
|
|
72
|
+
collate_fn=self._collate_fn,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def state_dict(self) -> dict[str, Any]:
|
|
76
|
+
"""Get dataset state for checkpointing."""
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
def load_state_dict(self, state_dict: dict[str, Any]) -> None:
|
|
80
|
+
"""Load dataset state from checkpoint."""
|
|
81
|
+
del state_dict # DummyDataset has no state to load
|
|
82
|
+
|
|
83
|
+
def _collate_fn(self, batch: list[tuple[Tensor, ...]]) -> dict[str, Tensor]:
|
|
84
|
+
"""Collate batch into dict format.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
batch: List of (data, label) tuples.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
dict: Batch dict with 'media' and 'label' keys.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
data_list, label_list = zip(*batch, strict=True)
|
|
94
|
+
device = torch.device(self.config.device)
|
|
95
|
+
return {
|
|
96
|
+
"media": torch.stack(data_list).to(device),
|
|
97
|
+
"label": torch.stack(label_list).to(device),
|
|
98
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## Glossary
|
|
2
|
+
|
|
3
|
+
See [PyTorch Distributed Overview](https://docs.pytorch.org/tutorials/beginner/dist_overview.html).
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
| Name | | Shard | Description |
|
|
7
|
+
|------------|------|-------|--------------|
|
|
8
|
+
| Rank | | | Single GPU |
|
|
9
|
+
| Node | | Rank | Group of GPUs with fast interconnection (same host; nvlink). |
|
|
10
|
+
| Cluster | | Node | Group of Nodes. |
|
|
11
|
+
| DDP | [Distributed Data Parallel](https://docs.pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html) | Model-per-Rank | Each rank ("GPU") owns a model replica and processes a microbatch of data. An all-reduce syncs gradients across ranks (forming a minibatch). |
|
|
12
|
+
| HSDP | [Hybrid Sharded Data Parallel](https://docs.pytorch.org/tutorials/recipes/distributed_device_mesh.html) | Model-per-Node | Combination of FSDP and DDP. Model sharded using FSDP _within each node_ (e.g., between the 8 GPUs on a single host) and data sharded across nodes |
|
|
13
|
+
| FSDP2 | [Fully Sharded Data Parallel](https://docs.pytorch.org/tutorials/intermediate/FSDP_tutorial.html) | Model-per-Cluster | Parameters, gradients, and optimizer states are sharded across all available GPUs in a cluster, regardless of which machine they are on. |
|
|
14
|
+
| TP | [Tensor Parallel](https://docs.pytorch.org/docs/stable/distributed.tensor.parallel.html) | | |
|
|
15
|
+
| PP | [Pipeline Parallel](https://docs.pytorch.org/docs/main/distributed.pipelining.html) | | Execution of a model to be partitioned such that multiple micro-batches can execute different parts of the model code concurrently. |
|
|
16
|
+
|
|
17
|
+
Rule-of-thumb: choose the smallest model sharding possible.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
| | DDP ([Distributed Data Parallel](https://docs.pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html)) | TP ([Tensor Parallel](https://docs.pytorch.org/docs/stable/distributed.tensor.parallel.html)) | FSDP ([Fully Sharded Data Parallel](https://docs.pytorch.org/tutorials/intermediate/FSDP_tutorial.html)) | HSDP ([Hybrid Sharded Data Parallel](https://docs.pytorch.org/tutorials/recipes/distributed_device_mesh.html)) | PP ([Pipeline Parallel](https://docs.pytorch.org/docs/main/distributed.pipelining.html)) |
|
|
22
|
+
|-------------|---|---|---|---|---|
|
|
23
|
+
Strategy | Data Parallelism. Each device gets a different slice of the input data. | Model Parallelism. A single layer's tensors are split across multiple devices. | Data Parallelism with Sharding. Combines data parallelism with sharding of the model state. | Hybrid Data Parallelism. Combines data parallelism with intra-node sharding. | Model Parallelism. The model is split by layer and distributed across devices. |
|
|
24
|
+
|||||||
|
|
25
|
+
Model State | The entire model (parameters, gradients, and optimizer states) is replicated on every device. | The model's weights and activations for a given layer are partitioned across a group of devices. | All model states (parameters, gradients, and optimizer states) are sharded across all devices. | Model parameters, gradients, and optimizer states are sharded within nodes but replicated between nodes. | Each device holds and computes only a contiguous sequence of model layers.
|
|
26
|
+
|||||||
|
|
27
|
+
Communication | Gradients are synchronized via an all-reduce operation after the backward pass. | Requires frequent intra-layer communication between devices to reassemble and compute split tensors. Best with high-speed interconnects like NVLink. | Requires communication (all-gather) to reconstruct parameters before computation and communication (reduce-scatter) to synchronize sharded gradients during the backward pass. | Balances communication costs by sharding across high-bandwidth intra-node connections while using replication across slower inter-node connections. | Activations are passed from one device to the next during the forward pass, and gradients are passed back during the backward pass.
|
|
28
|
+
|||||||
|
|
29
|
+
Advantage | High computational efficiency and simple to implement, as it requires minimal model code changes. | Enables parallelization for single layers that are too large for a single GPU, even with a small batch size. | Enables training models that are too large for a single GPU by significantly reducing per-device memory usage. | More flexible and scalable than FSDP, offering a balance between memory efficiency and communication overhead. | Allows training of extremely deep models by reducing the memory required per device.
|
|
30
|
+
|||||||
|
|
31
|
+
Disadvantage | Memory intensive, as every device must store a full copy of the model. | Adds communication overhead and can be complex to implement within the model architecture. | Higher communication overhead than DDP, and the all-gather operation can cause memory spikes. | Can be complex to configure optimally across different hardware topologies. | Introduces "pipeline bubbles"—periods of idle time—due to dependencies between stages.
|
|
32
|
+
|||||||
|
|
33
|
+
Best For | Training models that comfortably fit in the memory of a single GPU, using a larger batch size. | Large models where individual layers (e.g., in Transformers) are too big for a single GPU. | Very large models that exceed single-GPU memory limits, where memory savings are critical. | Large models that need to be scaled across many nodes with slower inter-node communication. | Extremely deep models where layers can be arranged sequentially. Often combined with microbatching to reduce idle time.
|
|
34
|
+
|||||||
|
|
35
|
+
Flexibility | Lowest flexibility. Scales only the data dimension. | Low flexibility. The model architecture must be explicitly modified to split tensors. | High flexibility. Can be applied at different levels of sharding and supports offloading to CPU for greater memory savings. | Medium-to-High flexibility. Offers a configurable sharding degree, adapting to different communication topologies. | Medium flexibility. Requires splitting the model layers into sequential stages.
|
|
36
|
+
|
|
37
|
+
Recommended strategy for large models and NVLink: HSDP with per-layer TP, i.e,
|
|
38
|
+
- One node is one microbatch.
|
|
39
|
+
- Shard across heads?
|