spmd_types 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {spmd_types-0.2.2/spmd_types.egg-info → spmd_types-0.2.4}/PKG-INFO +1 -2
- {spmd_types-0.2.2 → spmd_types-0.2.4}/pyproject.toml +1 -2
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/__init__.py +7 -5
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_backward_hooks.py +4 -16
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_checker/__init__.py +87 -34
- spmd_types-0.2.4/spmd_types/_local_registration.py +50 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_mesh.py +95 -10
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_state.py +24 -14
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/checker.py +2 -2
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/runtime.py +197 -81
- {spmd_types-0.2.2 → spmd_types-0.2.4/spmd_types.egg-info}/PKG-INFO +1 -2
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types.egg-info/SOURCES.txt +1 -1
- spmd_types-0.2.2/spmd_types.egg-info/requires.txt +0 -1
- {spmd_types-0.2.2 → spmd_types-0.2.4}/LICENSE +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/README.md +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/setup.cfg +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_collectives.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dist.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dtensor.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dtensor_checker.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dtype_utils.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_frame.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_local.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_mesh_axis.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_mesh_region.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_raw_dist.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_reinterpret_mesh.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_scalar.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_scalar_sentinel.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_test_utils.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_testing.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_traceback.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_type_attr.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_vmap.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/types.py +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types.egg-info/dependency_links.txt +0 -0
- {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spmd_types
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A type system for distributed (SPMD) tensor computations in PyTorch
|
|
5
5
|
Author: Meta Platforms, Inc.
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -47,7 +47,6 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
47
47
|
Requires-Python: >=3.10
|
|
48
48
|
Description-Content-Type: text/markdown
|
|
49
49
|
License-File: LICENSE
|
|
50
|
-
Requires-Dist: torch>=2.10.0
|
|
51
50
|
Dynamic: license-file
|
|
52
51
|
|
|
53
52
|
# `spmd_types`
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "spmd_types"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "A type system for distributed (SPMD) tensor computations in PyTorch"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
@@ -24,7 +24,6 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
-
"torch>=2.10.0",
|
|
28
27
|
]
|
|
29
28
|
|
|
30
29
|
[project.urls]
|
|
@@ -7,9 +7,6 @@
|
|
|
7
7
|
# spmd_types package
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from spmd_types._backward_hooks import ( # noqa: F401
|
|
11
|
-
register_local_backward_hook,
|
|
12
|
-
)
|
|
13
10
|
from spmd_types._collectives import ( # noqa: F401
|
|
14
11
|
all_gather,
|
|
15
12
|
all_reduce,
|
|
@@ -30,6 +27,10 @@ from spmd_types._local import ( # noqa: F401
|
|
|
30
27
|
reinterpret,
|
|
31
28
|
shard,
|
|
32
29
|
)
|
|
30
|
+
from spmd_types._local_registration import ( # noqa: F401
|
|
31
|
+
register_local_autograd_function,
|
|
32
|
+
register_local_backward_hook,
|
|
33
|
+
)
|
|
33
34
|
from spmd_types._mesh import set_current_mesh # noqa: F401
|
|
34
35
|
from spmd_types._mesh_axis import MeshAxis # noqa: F401
|
|
35
36
|
|
|
@@ -41,7 +42,6 @@ from spmd_types._state import ( # noqa: F401
|
|
|
41
42
|
current_mesh_all_names,
|
|
42
43
|
current_mesh_names,
|
|
43
44
|
is_type_checking,
|
|
44
|
-
no_typecheck,
|
|
45
45
|
)
|
|
46
46
|
from spmd_types._traceback import traceback_filtering # noqa: F401
|
|
47
47
|
from spmd_types._type_attr import ( # noqa: F401
|
|
@@ -56,13 +56,15 @@ from spmd_types.runtime import ( # noqa: F401
|
|
|
56
56
|
assert_local_type_like,
|
|
57
57
|
assert_type,
|
|
58
58
|
assert_type_like,
|
|
59
|
+
get_partition_spec,
|
|
60
|
+
has_local_type,
|
|
59
61
|
Infer,
|
|
60
62
|
local,
|
|
61
63
|
local_map,
|
|
62
64
|
mutate_type,
|
|
65
|
+
no_typecheck,
|
|
63
66
|
register_autograd_function,
|
|
64
67
|
register_decomposition,
|
|
65
|
-
register_local_autograd_function,
|
|
66
68
|
trace,
|
|
67
69
|
)
|
|
68
70
|
|
|
@@ -18,10 +18,12 @@ motivates the exact shape.
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
-
from collections.abc import Callable
|
|
22
|
-
|
|
23
21
|
import torch
|
|
24
22
|
import torch.utils.hooks as _torch_hooks
|
|
23
|
+
from spmd_types._local_registration import ( # noqa: F401
|
|
24
|
+
_LOCAL_BACKWARD_HOOKS,
|
|
25
|
+
register_local_backward_hook,
|
|
26
|
+
)
|
|
25
27
|
from spmd_types._state import is_type_checking
|
|
26
28
|
from spmd_types.runtime import (
|
|
27
29
|
_set_partition_spec,
|
|
@@ -31,20 +33,6 @@ from spmd_types.runtime import (
|
|
|
31
33
|
from spmd_types.types import SpmdTypeError
|
|
32
34
|
from torch.nn.modules._functions import BackwardHookFunction
|
|
33
35
|
|
|
34
|
-
_LOCAL_BACKWARD_HOOKS: set[Callable] = set()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def register_local_backward_hook(fn: Callable) -> Callable:
|
|
38
|
-
"""Declare that ``fn`` does not alter SPMD types when it runs in backward.
|
|
39
|
-
|
|
40
|
-
Analogous to :func:`register_local_autograd_function`: the hook's backward
|
|
41
|
-
is treated as local (no collectives, no type-changing effects on grads).
|
|
42
|
-
Side-agnostic: covers both ``register_full_backward_hook`` (post) and
|
|
43
|
-
``register_full_backward_pre_hook`` (pre) use. Usable as a decorator.
|
|
44
|
-
"""
|
|
45
|
-
_LOCAL_BACKWARD_HOOKS.add(fn)
|
|
46
|
-
return fn
|
|
47
|
-
|
|
48
36
|
|
|
49
37
|
def _validate(hooks):
|
|
50
38
|
if not is_type_checking():
|
|
@@ -36,6 +36,10 @@ from spmd_types._collectives import (
|
|
|
36
36
|
reduce_scatter,
|
|
37
37
|
)
|
|
38
38
|
from spmd_types._local import convert, invariant_to_replicate, reinterpret
|
|
39
|
+
from spmd_types._local_registration import ( # noqa: F401
|
|
40
|
+
_LOCAL_AUTOGRAD_FUNCTIONS,
|
|
41
|
+
register_local_autograd_function,
|
|
42
|
+
)
|
|
39
43
|
from spmd_types._mesh_axis import MeshAxis
|
|
40
44
|
from spmd_types._reinterpret_mesh import ( # noqa: F401
|
|
41
45
|
_format_arg_for_context,
|
|
@@ -53,8 +57,9 @@ from spmd_types._state import _current_mode, _set_current_mode, current_mesh
|
|
|
53
57
|
from spmd_types._traceback import _filter_and_reraise
|
|
54
58
|
from spmd_types._type_attr import get_local_type
|
|
55
59
|
from spmd_types.runtime import ( # noqa: F401
|
|
56
|
-
|
|
60
|
+
_get_autograd_spmd_typecheck,
|
|
57
61
|
_PARTITION_SPEC_ATTR,
|
|
62
|
+
_run_autograd_spmd_typecheck,
|
|
58
63
|
_set_local_type,
|
|
59
64
|
_set_partition_spec,
|
|
60
65
|
_TRACE,
|
|
@@ -68,9 +73,9 @@ from spmd_types.runtime import ( # noqa: F401
|
|
|
68
73
|
get_partition_spec,
|
|
69
74
|
has_local_type,
|
|
70
75
|
mutate_type,
|
|
76
|
+
no_typecheck,
|
|
71
77
|
register_autograd_function,
|
|
72
78
|
register_decomposition,
|
|
73
|
-
register_local_autograd_function,
|
|
74
79
|
trace,
|
|
75
80
|
)
|
|
76
81
|
from spmd_types.types import (
|
|
@@ -2454,10 +2459,9 @@ from spmd_types._state import is_type_checking # noqa: E402, F401
|
|
|
2454
2459
|
# Set of autograd.Function subclasses whose apply is known to be local-only
|
|
2455
2460
|
# (i.e., each output element depends only on the corresponding input elements
|
|
2456
2461
|
# so the standard element-wise type propagation rule is safe).
|
|
2457
|
-
# Set of autograd.Function subclasses registered with
|
|
2458
|
-
#
|
|
2459
|
-
#
|
|
2460
|
-
# on inputs, call .apply() to execute, and assert_type() on the output.
|
|
2462
|
+
# Set of autograd.Function subclasses registered with custom typechecking.
|
|
2463
|
+
# Legacy typecheck_forward wrappers execute the function themselves, while
|
|
2464
|
+
# spmd_typecheck runs after the real apply call managed by the checker.
|
|
2461
2465
|
|
|
2462
2466
|
|
|
2463
2467
|
# The original C++ descriptor for autograd.Function.apply, saved when the
|
|
@@ -2531,6 +2535,8 @@ def _get_autograd_function_class(func) -> type | None:
|
|
|
2531
2535
|
def _validate_partition_spec_for_global_spmd(
|
|
2532
2536
|
local_type: LocalSpmdType,
|
|
2533
2537
|
spec: PartitionSpec | None,
|
|
2538
|
+
*,
|
|
2539
|
+
is_global_axis: Callable[[MeshAxis], bool],
|
|
2534
2540
|
) -> None:
|
|
2535
2541
|
"""Validate PartitionSpec <-> local type consistency.
|
|
2536
2542
|
|
|
@@ -2543,10 +2549,12 @@ def _validate_partition_spec_for_global_spmd(
|
|
|
2543
2549
|
Args:
|
|
2544
2550
|
local_type: The tensor's local SPMD type dict.
|
|
2545
2551
|
spec: The tensor's PartitionSpec (None if absent).
|
|
2552
|
+
is_global_axis: Returns whether an axis requires global PartitionSpec
|
|
2553
|
+
validation in the current mesh context.
|
|
2546
2554
|
"""
|
|
2547
2555
|
spec_axes = spec.axes_with_partition_spec() if spec is not None else set()
|
|
2548
2556
|
for axis, typ in local_type.items():
|
|
2549
|
-
if typ is V and axis not in spec_axes:
|
|
2557
|
+
if is_global_axis(axis) and typ is V and axis not in spec_axes:
|
|
2550
2558
|
ax = format_axis(axis)
|
|
2551
2559
|
raise SpmdTypeError(
|
|
2552
2560
|
f"Tensor has Varying type on axis {ax} but no PartitionSpec entry. "
|
|
@@ -2555,7 +2563,7 @@ def _validate_partition_spec_for_global_spmd(
|
|
|
2555
2563
|
)
|
|
2556
2564
|
for axis in spec_axes:
|
|
2557
2565
|
typ = local_type.get(axis)
|
|
2558
|
-
if typ is not V:
|
|
2566
|
+
if is_global_axis(axis) and typ is not V:
|
|
2559
2567
|
ax = format_axis(axis)
|
|
2560
2568
|
raise SpmdTypeError(
|
|
2561
2569
|
f"Tensor has PartitionSpec on axis {ax} but local type is "
|
|
@@ -2623,6 +2631,23 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
|
|
|
2623
2631
|
|
|
2624
2632
|
self._disabled = False
|
|
2625
2633
|
|
|
2634
|
+
def _is_global_axis(self, axis: MeshAxis) -> bool:
|
|
2635
|
+
if self._local:
|
|
2636
|
+
return False
|
|
2637
|
+
local_axes = _state._current_local_axes()
|
|
2638
|
+
if not local_axes:
|
|
2639
|
+
return True
|
|
2640
|
+
mesh = _state.current_mesh()
|
|
2641
|
+
if mesh is None or axis not in mesh:
|
|
2642
|
+
raise SpmdTypeError(
|
|
2643
|
+
f"Axis {format_axis(axis)} is not in the current mesh, so its "
|
|
2644
|
+
"local/global typechecking policy is undefined"
|
|
2645
|
+
)
|
|
2646
|
+
return axis not in local_axes
|
|
2647
|
+
|
|
2648
|
+
def _global_axes_for(self, axes: Iterable[MeshAxis]) -> set[MeshAxis]:
|
|
2649
|
+
return {axis for axis in axes if self._is_global_axis(axis)}
|
|
2650
|
+
|
|
2626
2651
|
def __torch_function__(self, func, types, args=(), kwargs=None): # noqa: C901
|
|
2627
2652
|
kwargs = kwargs or {}
|
|
2628
2653
|
# Paused via no_typecheck(): run without type checking.
|
|
@@ -2679,20 +2704,28 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
|
|
|
2679
2704
|
# Global SPMD validation.
|
|
2680
2705
|
if not self._local:
|
|
2681
2706
|
for local_type, spec in zip(info.tensor_types, info.partition_specs):
|
|
2682
|
-
_validate_partition_spec_for_global_spmd(
|
|
2707
|
+
_validate_partition_spec_for_global_spmd(
|
|
2708
|
+
local_type,
|
|
2709
|
+
spec,
|
|
2710
|
+
is_global_axis=self._is_global_axis,
|
|
2711
|
+
)
|
|
2683
2712
|
|
|
2684
|
-
# Typecheck-registered autograd function
|
|
2685
|
-
# INSTEAD of executing func. The mode is popped off the
|
|
2713
|
+
# Typecheck-registered autograd function. The mode is popped off the
|
|
2686
2714
|
# TorchFunctionMode stack during __torch_function__ dispatch (via
|
|
2687
|
-
# _pop_mode_temporarily in torch/overrides.py), so
|
|
2688
|
-
#
|
|
2689
|
-
# this method.
|
|
2715
|
+
# _pop_mode_temporarily in torch/overrides.py), so the real apply
|
|
2716
|
+
# executes normally without re-entering this method.
|
|
2690
2717
|
autograd_cls = _get_autograd_function_class(func)
|
|
2691
|
-
if
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2718
|
+
if autograd_cls is not None:
|
|
2719
|
+
spmd_typecheck = _get_autograd_spmd_typecheck(autograd_cls)
|
|
2720
|
+
if spmd_typecheck is not None:
|
|
2721
|
+
if kwargs:
|
|
2722
|
+
raise TypeError(
|
|
2723
|
+
f"{autograd_cls.__name__}.apply() accepts positional "
|
|
2724
|
+
"arguments only"
|
|
2725
|
+
)
|
|
2726
|
+
return _run_autograd_spmd_typecheck(autograd_cls, func, args)
|
|
2727
|
+
if autograd_cls in _TYPECHECK_AUTOGRAD_FUNCTIONS:
|
|
2728
|
+
return autograd_cls.typecheck_forward(*args, **kwargs)
|
|
2696
2729
|
|
|
2697
2730
|
# DTensor tracks its own placement metadata; the SPMD type checker
|
|
2698
2731
|
# should not interfere with DTensor operations. This covers
|
|
@@ -2744,6 +2777,29 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
|
|
|
2744
2777
|
# Resolve axis to a list of matching axes on the tensor.
|
|
2745
2778
|
# Direct match: [axis]. Flattened decomposition: [sub1, sub2, ...].
|
|
2746
2779
|
resolved_axes = _resolve_collective_axes(axis, local_type)
|
|
2780
|
+
collective_axes = resolved_axes or [axis]
|
|
2781
|
+
global_collective_axes = [
|
|
2782
|
+
ax for ax in collective_axes if self._is_global_axis(ax)
|
|
2783
|
+
]
|
|
2784
|
+
if global_collective_axes and len(global_collective_axes) != len(
|
|
2785
|
+
collective_axes
|
|
2786
|
+
):
|
|
2787
|
+
global_axis_set = set(global_collective_axes)
|
|
2788
|
+
local_collective_axes = [
|
|
2789
|
+
ax for ax in collective_axes if ax not in global_axis_set
|
|
2790
|
+
]
|
|
2791
|
+
local_names = ", ".join(
|
|
2792
|
+
format_axis(ax) for ax in local_collective_axes
|
|
2793
|
+
)
|
|
2794
|
+
global_names = ", ".join(
|
|
2795
|
+
format_axis(ax) for ax in global_collective_axes
|
|
2796
|
+
)
|
|
2797
|
+
raise SpmdTypeError(
|
|
2798
|
+
f"Collective axis {format_axis(axis)} spans mesh axes with "
|
|
2799
|
+
f"mixed typechecking policies: local axes ({local_names}) "
|
|
2800
|
+
f"and global axes ({global_names}). Pass a collective axis "
|
|
2801
|
+
f"whose component axes are all local or all global."
|
|
2802
|
+
)
|
|
2747
2803
|
|
|
2748
2804
|
if resolved_axes is not None:
|
|
2749
2805
|
# Validate input types on all resolved axes
|
|
@@ -2774,7 +2830,7 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
|
|
|
2774
2830
|
output_type[ax] = local_dst
|
|
2775
2831
|
_set_local_type(result, output_type)
|
|
2776
2832
|
|
|
2777
|
-
if
|
|
2833
|
+
if global_collective_axes:
|
|
2778
2834
|
input_spec = get_partition_spec(x)
|
|
2779
2835
|
# Find S(dim) for this axis in the input spec, validating
|
|
2780
2836
|
# it is innermost in its multi-axis group.
|
|
@@ -2859,10 +2915,9 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
|
|
|
2859
2915
|
f"checking. Use "
|
|
2860
2916
|
f"{register_local_autograd_function.__name__}("
|
|
2861
2917
|
f"{autograd_cls.__name__}) to mark it as safe "
|
|
2862
|
-
f"for type propagation,
|
|
2863
|
-
f"
|
|
2864
|
-
f"
|
|
2865
|
-
f"typecheck_forward."
|
|
2918
|
+
f"for type propagation, or define "
|
|
2919
|
+
f"{autograd_cls.__name__}.spmd_typecheck for a custom "
|
|
2920
|
+
f"typecheck rule."
|
|
2866
2921
|
)
|
|
2867
2922
|
return result
|
|
2868
2923
|
|
|
@@ -2916,15 +2971,16 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
|
|
|
2916
2971
|
)
|
|
2917
2972
|
|
|
2918
2973
|
# Stage 3: global shard propagation.
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
all_axes.update(typ.keys())
|
|
2974
|
+
all_axes: set[MeshAxis] = set()
|
|
2975
|
+
for typ in input_types_list:
|
|
2976
|
+
all_axes.update(typ.keys())
|
|
2977
|
+
global_axes = self._global_axes_for(all_axes)
|
|
2978
|
+
if global_axes:
|
|
2925
2979
|
global_shard_axes, shard_edges = _collect_shard_axes(
|
|
2926
|
-
partition_specs,
|
|
2980
|
+
partition_specs, global_axes
|
|
2927
2981
|
)
|
|
2982
|
+
else:
|
|
2983
|
+
global_shard_axes, shard_edges = [], {}
|
|
2928
2984
|
|
|
2929
2985
|
# Validate mutation safety for in-place/out operations.
|
|
2930
2986
|
mutated = _get_mutated_tensors(func, args, kwargs, result)
|
|
@@ -3086,9 +3142,6 @@ def typecheck(
|
|
|
3086
3142
|
_remove_autograd_apply_patch()
|
|
3087
3143
|
|
|
3088
3144
|
|
|
3089
|
-
from spmd_types._state import no_typecheck # noqa: F401
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
3145
|
class _SpmdTypeBackwardCompatibleMode:
|
|
3093
3146
|
"""Backwards-compatible wrapper around ``typecheck()``.
|
|
3094
3147
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This source code is licensed under the BSD-style license found in the
|
|
5
|
+
# LICENSE file in the root directory of this source tree.
|
|
6
|
+
|
|
7
|
+
"""Lightweight registries for local SPMD type propagation."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
_LOCAL_AUTOGRAD_FUNCTIONS: set[type] = set()
|
|
14
|
+
_LOCAL_BACKWARD_HOOKS: set[Callable] = set()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_local_autograd_function(cls: type) -> type:
|
|
18
|
+
"""Register an autograd.Function subclass as local-only for SPMD type checking.
|
|
19
|
+
|
|
20
|
+
Local-only means the function's forward operates element-wise (or more
|
|
21
|
+
generally, does not rearrange data across the tensor in a way that would
|
|
22
|
+
change its sharding type). It must not perform collectives or cross-device
|
|
23
|
+
communication. For functions that do, use ``register_autograd_function``
|
|
24
|
+
with custom typechecking instead.
|
|
25
|
+
|
|
26
|
+
Registered functions get the standard local type propagation rule when
|
|
27
|
+
type checking is active:
|
|
28
|
+
|
|
29
|
+
- Inputs may freely mix R and V types; the output is R unless any input
|
|
30
|
+
is V, in which case it is V.
|
|
31
|
+
- All-I inputs produce I outputs.
|
|
32
|
+
- R/V and I cannot be mixed.
|
|
33
|
+
- P is forbidden.
|
|
34
|
+
|
|
35
|
+
Unregistered autograd functions that reach the type checker leave their
|
|
36
|
+
outputs untyped or raise in strict mode.
|
|
37
|
+
"""
|
|
38
|
+
_LOCAL_AUTOGRAD_FUNCTIONS.add(cls)
|
|
39
|
+
return cls
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register_local_backward_hook(fn: Callable) -> Callable:
|
|
43
|
+
"""Declare that ``fn`` does not alter SPMD types when it runs in backward.
|
|
44
|
+
|
|
45
|
+
Analogous to ``register_local_autograd_function``, the hook's backward is
|
|
46
|
+
treated as local with no collectives or type-changing effects on gradients.
|
|
47
|
+
This covers both full backward post-hooks and pre-hooks.
|
|
48
|
+
"""
|
|
49
|
+
_LOCAL_BACKWARD_HOOKS.add(fn)
|
|
50
|
+
return fn
|
|
@@ -13,8 +13,8 @@ from contextlib import contextmanager
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
15
|
from spmd_types._mesh_axis import MeshAxis
|
|
16
|
-
from spmd_types._state import _axes_to_pgs, _pop_mesh, _push_mesh
|
|
17
|
-
from spmd_types.types import DeviceMeshAxis, normalize_mesh
|
|
16
|
+
from spmd_types._state import _axes_to_pgs, _current_mesh_entry, _pop_mesh, _push_mesh
|
|
17
|
+
from spmd_types.types import DeviceMeshAxis, format_axis, normalize_axis, normalize_mesh
|
|
18
18
|
from torch.distributed.device_mesh import DeviceMesh
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
@@ -24,8 +24,14 @@ if TYPE_CHECKING:
|
|
|
24
24
|
@contextmanager
|
|
25
25
|
def set_current_mesh(
|
|
26
26
|
axes: (
|
|
27
|
-
frozenset[MeshAxis]
|
|
28
|
-
|
|
27
|
+
frozenset[MeshAxis]
|
|
28
|
+
| DeviceMesh
|
|
29
|
+
| Sequence[ProcessGroup]
|
|
30
|
+
| dict[str, MeshAxis]
|
|
31
|
+
| None
|
|
32
|
+
) = None,
|
|
33
|
+
*,
|
|
34
|
+
local_axes: tuple[DeviceMeshAxis, ...] = (),
|
|
29
35
|
):
|
|
30
36
|
"""Context manager that pushes a mesh onto the stack.
|
|
31
37
|
|
|
@@ -37,19 +43,99 @@ def set_current_mesh(
|
|
|
37
43
|
- A frozenset of orthogonal MeshAxis objects (no string lookup).
|
|
38
44
|
- A sequence of ProcessGroup objects, each converted via
|
|
39
45
|
``MeshAxis.of()`` (no string lookup).
|
|
46
|
+
When omitted, reuses the current mesh.
|
|
47
|
+
local_axes: An outer prefix of mesh axes that retains local SPMD
|
|
48
|
+
semantics during global type checking. Each coordinate on these
|
|
49
|
+
axes selects an independent local tensor; the remaining inner axes
|
|
50
|
+
describe the global sharding of that tensor. For example, with
|
|
51
|
+
``dp`` local and ``tp`` global, each ``dp`` coordinate has a
|
|
52
|
+
separate tensor globally sharded across ``tp``.
|
|
40
53
|
|
|
41
54
|
Singleton (size-1) axes are dropped from the active axis set via
|
|
42
55
|
``normalize_mesh`` but remain in the name lookup table
|
|
43
56
|
(``current_mesh_all_names``).
|
|
44
57
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
if axes is None:
|
|
59
|
+
entry = _current_mesh_entry()
|
|
60
|
+
if entry is None:
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
"set_current_mesh() without axes requires a current mesh"
|
|
63
|
+
)
|
|
64
|
+
resolved, names, pgs = entry.axes, entry.all_names, entry.pgs
|
|
65
|
+
else:
|
|
66
|
+
resolved, names = _resolve_axes(axes)
|
|
67
|
+
pgs = _axes_to_pgs(axes)
|
|
68
|
+
resolved_local_axes = _resolve_local_axes(local_axes, resolved, names)
|
|
69
|
+
_push_mesh(
|
|
70
|
+
resolved,
|
|
71
|
+
names,
|
|
72
|
+
pgs,
|
|
73
|
+
local_axes=resolved_local_axes,
|
|
74
|
+
)
|
|
47
75
|
try:
|
|
48
76
|
yield
|
|
49
77
|
finally:
|
|
50
78
|
_pop_mesh()
|
|
51
79
|
|
|
52
80
|
|
|
81
|
+
def _resolve_local_axes(
|
|
82
|
+
local_axes: tuple[DeviceMeshAxis, ...],
|
|
83
|
+
mesh: frozenset[MeshAxis],
|
|
84
|
+
names: dict[str, MeshAxis],
|
|
85
|
+
) -> frozenset[MeshAxis]:
|
|
86
|
+
"""Resolve ``local_axes`` to members of ``mesh``.
|
|
87
|
+
|
|
88
|
+
Rejects unknown or duplicate axes and requires the local axes to be the
|
|
89
|
+
outermost axes and the global axes to be the innermost axes. For a
|
|
90
|
+
``(dp, tp)`` mesh, ``("dp",)`` is valid but ``("tp",)`` is not.
|
|
91
|
+
|
|
92
|
+
Returns the resolved local axes as a ``frozenset[MeshAxis]``.
|
|
93
|
+
"""
|
|
94
|
+
unknown = tuple(
|
|
95
|
+
axis for axis in local_axes if isinstance(axis, str) and axis not in names
|
|
96
|
+
)
|
|
97
|
+
if unknown:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"local_axes contains names not present in this mesh: {unknown!r}; "
|
|
100
|
+
f"available names: {tuple(sorted(names))!r}"
|
|
101
|
+
)
|
|
102
|
+
resolved_list = tuple(
|
|
103
|
+
names[axis] if isinstance(axis, str) else normalize_axis(axis)
|
|
104
|
+
for axis in local_axes
|
|
105
|
+
)
|
|
106
|
+
if len(set(resolved_list)) != len(resolved_list):
|
|
107
|
+
duplicates = {axis for axis in resolved_list if resolved_list.count(axis) > 1}
|
|
108
|
+
duplicate_names = ", ".join(format_axis(axis) for axis in duplicates)
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"local_axes must not contain duplicate axes; got {local_axes!r}; "
|
|
111
|
+
f"duplicate axes: ({duplicate_names})"
|
|
112
|
+
)
|
|
113
|
+
resolved = frozenset(resolved_list)
|
|
114
|
+
if not resolved <= mesh:
|
|
115
|
+
outside = resolved - mesh
|
|
116
|
+
outside_names = ", ".join(format_axis(axis) for axis in outside)
|
|
117
|
+
mesh_names = ", ".join(format_axis(axis) for axis in sorted(mesh, key=repr))
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"local_axes contains axes outside this mesh: ({outside_names}); "
|
|
120
|
+
f"current mesh axes: ({mesh_names})"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
ordered = sorted(
|
|
124
|
+
mesh,
|
|
125
|
+
key=lambda axis: max(stride for _, stride in axis.layout.sizes_and_strides),
|
|
126
|
+
reverse=True,
|
|
127
|
+
)
|
|
128
|
+
prefix = set(ordered[: len(resolved)])
|
|
129
|
+
if set(resolved) != prefix:
|
|
130
|
+
local_names = ", ".join(format_axis(axis) for axis in resolved)
|
|
131
|
+
mesh_names = ", ".join(format_axis(axis) for axis in ordered)
|
|
132
|
+
raise ValueError(
|
|
133
|
+
"local_axes must come before all global axes in outer-to-inner mesh "
|
|
134
|
+
f"order; got local_axes=({local_names}) for mesh=({mesh_names})"
|
|
135
|
+
)
|
|
136
|
+
return resolved
|
|
137
|
+
|
|
138
|
+
|
|
53
139
|
def _pg_for_axis(axis: DeviceMeshAxis) -> ProcessGroup:
|
|
54
140
|
"""Resolve a mesh axis to its ``ProcessGroup`` using the current mesh.
|
|
55
141
|
|
|
@@ -57,14 +143,13 @@ def _pg_for_axis(axis: DeviceMeshAxis) -> ProcessGroup:
|
|
|
57
143
|
``MeshAxis``. Raises ``RuntimeError`` if no current mesh carries process
|
|
58
144
|
groups, or if the axis is not part of it.
|
|
59
145
|
"""
|
|
60
|
-
from spmd_types._state import _tls, current_mesh_all_names
|
|
61
146
|
from torch.distributed import ProcessGroup
|
|
62
147
|
|
|
63
148
|
if isinstance(axis, ProcessGroup):
|
|
64
149
|
return axis
|
|
65
150
|
|
|
66
|
-
|
|
67
|
-
pgs =
|
|
151
|
+
entry = _current_mesh_entry()
|
|
152
|
+
pgs = entry.pgs if entry is not None else None
|
|
68
153
|
if not pgs:
|
|
69
154
|
raise RuntimeError(
|
|
70
155
|
"Passing an axis name to a redistribution or collective "
|
|
@@ -73,7 +158,7 @@ def _pg_for_axis(axis: DeviceMeshAxis) -> ProcessGroup:
|
|
|
73
158
|
)
|
|
74
159
|
|
|
75
160
|
if isinstance(axis, str):
|
|
76
|
-
resolved =
|
|
161
|
+
resolved = entry.all_names.get(axis) if entry is not None else None
|
|
77
162
|
else:
|
|
78
163
|
resolved = axis
|
|
79
164
|
if resolved is None or resolved not in pgs:
|
|
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
|
|
|
30
30
|
|
|
31
31
|
class MeshEntry(NamedTuple):
|
|
32
32
|
axes: frozenset[MeshAxis]
|
|
33
|
+
local_axes: frozenset[MeshAxis]
|
|
33
34
|
names: dict[str, MeshAxis]
|
|
34
35
|
all_names: dict[str, MeshAxis]
|
|
35
36
|
pgs: dict[MeshAxis, ProcessGroup]
|
|
@@ -45,7 +46,7 @@ def is_type_checking() -> bool:
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
@contextmanager
|
|
48
|
-
def
|
|
49
|
+
def _no_typecheck_context():
|
|
49
50
|
"""Temporarily disable type checking on this thread (like ``no_grad``)."""
|
|
50
51
|
mode = _current_mode()
|
|
51
52
|
if mode is not None:
|
|
@@ -75,12 +76,16 @@ def is_strict() -> bool:
|
|
|
75
76
|
return mode is not None and mode._strict
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
def _current_mesh_entry() -> MeshEntry | None:
|
|
80
|
+
"""Return the active mesh-stack entry, or None if no mesh is set."""
|
|
81
|
+
stack: list[MeshEntry] = getattr(_tls, "mesh_stack", [])
|
|
82
|
+
return stack[-1] if stack else None
|
|
83
|
+
|
|
84
|
+
|
|
78
85
|
def current_mesh() -> frozenset[MeshAxis] | None:
|
|
79
86
|
"""Return the current mesh axes, or None if no mesh is set."""
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
return stack[-1].axes
|
|
83
|
-
return None
|
|
87
|
+
entry = _current_mesh_entry()
|
|
88
|
+
return entry.axes if entry is not None else None
|
|
84
89
|
|
|
85
90
|
|
|
86
91
|
def current_mesh_names() -> dict[str, MeshAxis] | None:
|
|
@@ -89,18 +94,14 @@ def current_mesh_names() -> dict[str, MeshAxis] | None:
|
|
|
89
94
|
Only includes non-singleton axes, consistent with ``current_mesh()``.
|
|
90
95
|
Use ``current_mesh_all_names()`` to include singleton axes.
|
|
91
96
|
"""
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
return stack[-1].names
|
|
95
|
-
return None
|
|
97
|
+
entry = _current_mesh_entry()
|
|
98
|
+
return entry.names if entry is not None else None
|
|
96
99
|
|
|
97
100
|
|
|
98
101
|
def current_mesh_all_names() -> dict[str, MeshAxis] | None:
|
|
99
102
|
"""Return the full name-to-axis mapping including singleton axes, or None."""
|
|
100
|
-
|
|
101
|
-
if
|
|
102
|
-
return stack[-1].all_names
|
|
103
|
-
return None
|
|
103
|
+
entry = _current_mesh_entry()
|
|
104
|
+
return entry.all_names if entry is not None else None
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
def _axes_to_pgs(
|
|
@@ -133,6 +134,7 @@ def _push_mesh(
|
|
|
133
134
|
| dict[str, MeshAxis],
|
|
134
135
|
names: dict[str, MeshAxis] | None = None,
|
|
135
136
|
pgs: dict[MeshAxis, ProcessGroup] | None = None,
|
|
137
|
+
local_axes: frozenset[MeshAxis] = frozenset(),
|
|
136
138
|
) -> None:
|
|
137
139
|
if isinstance(mesh, frozenset):
|
|
138
140
|
resolved, resolved_names = mesh, names if names is not None else {}
|
|
@@ -149,7 +151,9 @@ def _push_mesh(
|
|
|
149
151
|
filtered_names = {k: v for k, v in resolved_names.items() if v in resolved}
|
|
150
152
|
if not hasattr(_tls, "mesh_stack"):
|
|
151
153
|
_tls.mesh_stack = []
|
|
152
|
-
_tls.mesh_stack.append(
|
|
154
|
+
_tls.mesh_stack.append(
|
|
155
|
+
MeshEntry(resolved, local_axes, filtered_names, all_names, resolved_pgs)
|
|
156
|
+
)
|
|
153
157
|
|
|
154
158
|
|
|
155
159
|
def _pop_mesh() -> MeshEntry:
|
|
@@ -166,6 +170,12 @@ def _clear_mesh_stack() -> None:
|
|
|
166
170
|
_tls.mesh_stack = []
|
|
167
171
|
|
|
168
172
|
|
|
173
|
+
def _current_local_axes() -> frozenset[MeshAxis]:
|
|
174
|
+
"""Return the local axes for the active mesh, or an empty set."""
|
|
175
|
+
entry = _current_mesh_entry()
|
|
176
|
+
return entry.local_axes if entry is not None else frozenset()
|
|
177
|
+
|
|
178
|
+
|
|
169
179
|
def _is_global() -> bool:
|
|
170
180
|
"""Return True if global SPMD mode is active (local=False)."""
|
|
171
181
|
mode = _current_mode()
|
|
@@ -9,5 +9,5 @@ from spmd_types._checker import ( # noqa: F401
|
|
|
9
9
|
_SpmdTypeBackwardCompatibleMode as SpmdTypeMode,
|
|
10
10
|
typecheck,
|
|
11
11
|
)
|
|
12
|
-
from spmd_types._state import is_type_checking
|
|
13
|
-
from spmd_types.runtime import local_map # noqa: F401
|
|
12
|
+
from spmd_types._state import is_type_checking # noqa: F401
|
|
13
|
+
from spmd_types.runtime import local_map, no_typecheck # noqa: F401
|
|
@@ -25,16 +25,20 @@ need the annotation APIs.
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import builtins
|
|
28
|
+
import inspect
|
|
28
29
|
import logging
|
|
29
30
|
import os
|
|
30
|
-
from contextlib import contextmanager
|
|
31
|
-
from typing import Any, TypeAlias
|
|
31
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
32
|
+
from typing import Any, Callable, overload, TypeAlias
|
|
32
33
|
|
|
33
34
|
import torch
|
|
34
35
|
from spmd_types._frame import _get_user_frame
|
|
36
|
+
from spmd_types._local_registration import ( # noqa: F401
|
|
37
|
+
register_local_autograd_function,
|
|
38
|
+
)
|
|
35
39
|
from spmd_types._mesh_axis import MeshAxis
|
|
36
40
|
from spmd_types._scalar_sentinel import _Scalar
|
|
37
|
-
from spmd_types._state import current_mesh, is_type_checking
|
|
41
|
+
from spmd_types._state import _no_typecheck_context, current_mesh, is_type_checking
|
|
38
42
|
from spmd_types._traceback import api_boundary
|
|
39
43
|
from spmd_types._type_attr import (
|
|
40
44
|
_LOCAL_TYPE_ATTR,
|
|
@@ -589,20 +593,49 @@ def assert_local_type_like(
|
|
|
589
593
|
# =============================================================================
|
|
590
594
|
|
|
591
595
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
tensor: torch.Tensor,
|
|
596
|
+
def _mutate_type_single_axis(
|
|
597
|
+
local_type: LocalSpmdType,
|
|
595
598
|
axis: DeviceMeshAxis,
|
|
596
599
|
*,
|
|
597
600
|
src: PerMeshAxisSpmdType,
|
|
598
601
|
dst: PerMeshAxisSpmdType,
|
|
602
|
+
) -> LocalSpmdType:
|
|
603
|
+
axis = normalize_axis(axis)
|
|
604
|
+
if axis.size() == 1:
|
|
605
|
+
return local_type
|
|
606
|
+
if axis not in local_type:
|
|
607
|
+
raise SpmdTypeError(
|
|
608
|
+
f"mutate_type: axis {format_axis(axis)} not found in tensor's "
|
|
609
|
+
f"SPMD type. Tensor has axes: {set(local_type.keys())}"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
src_local = to_local_type(src)
|
|
613
|
+
current = local_type[axis]
|
|
614
|
+
if current is not src_local:
|
|
615
|
+
raise SpmdTypeError(
|
|
616
|
+
f"mutate_type: expected current type {src_local} on axis "
|
|
617
|
+
f"{format_axis(axis)}, got {current}"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
new_type = dict(local_type)
|
|
621
|
+
new_type[axis] = to_local_type(dst)
|
|
622
|
+
return new_type
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@api_boundary
|
|
626
|
+
def mutate_type(
|
|
627
|
+
tensor: torch.Tensor,
|
|
628
|
+
axis: DeviceMeshAxis | None = None,
|
|
629
|
+
*,
|
|
630
|
+
src: PerMeshAxisSpmdType | dict[DeviceMeshAxis, PerMeshAxisSpmdType],
|
|
631
|
+
dst: PerMeshAxisSpmdType | dict[DeviceMeshAxis, PerMeshAxisSpmdType],
|
|
599
632
|
) -> torch.Tensor:
|
|
600
|
-
"""Change the SPMD type of
|
|
633
|
+
"""Change the SPMD type of mesh axes on an already-annotated tensor.
|
|
601
634
|
|
|
602
635
|
Unlike ``assert_type``, this function *overwrites* the existing type on
|
|
603
|
-
|
|
604
|
-
prevent silent corruption; a ``SpmdTypeError`` is raised if
|
|
605
|
-
current type
|
|
636
|
+
the updated axes. The caller must specify the expected current type
|
|
637
|
+
(``src``) to prevent silent corruption; a ``SpmdTypeError`` is raised if
|
|
638
|
+
an updated axis's current type does not match its expected ``src``.
|
|
606
639
|
|
|
607
640
|
This is intended for internal use in low-level parallelism primitives
|
|
608
641
|
where a buffer legitimately changes its distribution semantics in place
|
|
@@ -613,45 +646,88 @@ def mutate_type(
|
|
|
613
646
|
*views* into a buffer after an in-place collective has changed the
|
|
614
647
|
buffer's semantics.
|
|
615
648
|
|
|
649
|
+
There are four supported call shapes:
|
|
650
|
+
|
|
651
|
+
- ``mutate_type(tensor, axis, src=R, dst=I)`` updates a single axis.
|
|
652
|
+
- ``mutate_type(tensor, src=R, dst=I)`` updates all axes annotated on the
|
|
653
|
+
tensor, assuming all axes share ``src`` type.
|
|
654
|
+
- ``mutate_type(tensor, src=R, dst={axis0: I, axis1: P})`` updates every
|
|
655
|
+
axis in ``dst``, after checking all update axes have ``src`` type.
|
|
656
|
+
Axes not listed in ``dst`` are unaffected.
|
|
657
|
+
- ``mutate_type(tensor, src={axis0: R, axis1: I}, dst=P)`` is similarly supported.
|
|
658
|
+
- ``mutate_type(tensor, src={axis0: R, axis1: R}, dst={axis0: I, axis1: I})``
|
|
659
|
+
updates each axis correspondingly. ``src`` and ``dst`` keys must match,
|
|
660
|
+
and other axes are unaffected.
|
|
661
|
+
|
|
662
|
+
``mutate_type`` errors if any specified ``src`` type doesn't match the tensor's
|
|
663
|
+
annotation.
|
|
664
|
+
|
|
616
665
|
Args:
|
|
617
666
|
tensor: The tensor whose type to mutate. Must already have a local
|
|
618
667
|
type annotation.
|
|
619
|
-
axis: The mesh axis (MeshAxis or ProcessGroup) to modify
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
(S(i) is
|
|
668
|
+
axis: The mesh axis (MeshAxis or ProcessGroup) to modify for the
|
|
669
|
+
single-axis form. Omit when ``src`` or ``dst`` is a dict mapping
|
|
670
|
+
axes to types.
|
|
671
|
+
src: The expected current type, or a dict of per-axis expected current
|
|
672
|
+
types. Accepts R, I, V, P, or S(i); S(i) is compared as V.
|
|
673
|
+
dst: The new type, or a dict of per-axis destination types. Accepts R,
|
|
674
|
+
I, V, P, or S(i); S(i) is stored as V.
|
|
624
675
|
|
|
625
676
|
Returns:
|
|
626
677
|
The tensor for chaining.
|
|
627
678
|
|
|
628
679
|
Raises:
|
|
629
|
-
SpmdTypeError: If the tensor has no type,
|
|
630
|
-
|
|
680
|
+
SpmdTypeError: If the tensor has no type, if a non-singleton updated
|
|
681
|
+
axis is missing from the tensor's local type, if a current type
|
|
682
|
+
does not match ``src``, or if per-axis ``src`` and ``dst`` dict keys
|
|
683
|
+
differ.
|
|
631
684
|
"""
|
|
632
|
-
axis = normalize_axis(axis)
|
|
633
|
-
if axis.size() == 1:
|
|
634
|
-
return tensor # singleton axes are not tracked
|
|
635
685
|
local_type = get_local_type(tensor)
|
|
636
|
-
if
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
686
|
+
if isinstance(dst, dict) and isinstance(src, dict):
|
|
687
|
+
if axis is not None:
|
|
688
|
+
raise SpmdTypeError(
|
|
689
|
+
"mutate_type() expected axis to be omitted when src or dst is a dict."
|
|
690
|
+
)
|
|
691
|
+
if set(dst) != set(src):
|
|
692
|
+
axes = ", ".join(format_axis(axis) for axis in set(dst) ^ set(src))
|
|
693
|
+
raise SpmdTypeError(
|
|
694
|
+
f"mutate_type() expected src and dst axes to match: {axes}"
|
|
695
|
+
)
|
|
696
|
+
updates = [
|
|
697
|
+
(update_axis, src[update_axis], dst_type)
|
|
698
|
+
for update_axis, dst_type in dst.items()
|
|
699
|
+
]
|
|
700
|
+
elif isinstance(dst, dict):
|
|
701
|
+
if axis is not None:
|
|
702
|
+
raise SpmdTypeError(
|
|
703
|
+
"mutate_type() expected axis to be omitted when src or dst is a dict."
|
|
704
|
+
)
|
|
705
|
+
updates = [
|
|
706
|
+
(update_axis, src, dst_type) for update_axis, dst_type in dst.items()
|
|
707
|
+
]
|
|
708
|
+
elif isinstance(src, dict):
|
|
709
|
+
if axis is not None:
|
|
710
|
+
raise SpmdTypeError(
|
|
711
|
+
"mutate_type() expected axis to be omitted when src or dst is a dict."
|
|
712
|
+
)
|
|
713
|
+
updates = [
|
|
714
|
+
(update_axis, src_type, dst) for update_axis, src_type in src.items()
|
|
715
|
+
]
|
|
716
|
+
else:
|
|
717
|
+
updates = (
|
|
718
|
+
[(axis, src, dst)]
|
|
719
|
+
if axis is not None
|
|
720
|
+
else [(update_axis, src, dst) for update_axis in local_type.keys()]
|
|
651
721
|
)
|
|
652
722
|
|
|
653
723
|
new_type = dict(local_type)
|
|
654
|
-
|
|
724
|
+
for update_axis, src_type, dst_type in updates:
|
|
725
|
+
new_type = _mutate_type_single_axis(
|
|
726
|
+
new_type,
|
|
727
|
+
update_axis,
|
|
728
|
+
src=src_type,
|
|
729
|
+
dst=dst_type,
|
|
730
|
+
)
|
|
655
731
|
return _set_local_type(tensor, new_type)
|
|
656
732
|
|
|
657
733
|
|
|
@@ -659,43 +735,37 @@ def mutate_type(
|
|
|
659
735
|
# Autograd function registration
|
|
660
736
|
# =============================================================================
|
|
661
737
|
|
|
662
|
-
#
|
|
663
|
-
# _LOCAL_AUTOGRAD_FUNCTIONS: element-wise / local-only functions.
|
|
664
|
-
# _TYPECHECK_AUTOGRAD_FUNCTIONS: functions with custom typecheck_forward.
|
|
665
|
-
_LOCAL_AUTOGRAD_FUNCTIONS: set[type] = set()
|
|
738
|
+
# Autograd.Function subclasses with custom typechecking.
|
|
666
739
|
_TYPECHECK_AUTOGRAD_FUNCTIONS: set[type] = set()
|
|
667
740
|
|
|
668
741
|
|
|
669
|
-
def
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
742
|
+
def _run_autograd_spmd_typecheck(
|
|
743
|
+
cls: type,
|
|
744
|
+
func: Callable[..., object],
|
|
745
|
+
args: tuple[object, ...],
|
|
746
|
+
) -> object:
|
|
747
|
+
signature = inspect.signature(cls.forward)
|
|
748
|
+
has_ctx = cls.setup_context is torch.autograd.Function.setup_context
|
|
749
|
+
bound = signature.bind(*(args if not has_ctx else (None, *args)))
|
|
750
|
+
bound.apply_defaults()
|
|
751
|
+
forward_args = dict(bound.arguments)
|
|
752
|
+
if has_ctx:
|
|
753
|
+
forward_args.pop(next(iter(signature.parameters)))
|
|
680
754
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
- P is forbidden.
|
|
755
|
+
outputs = func(*args)
|
|
756
|
+
hook = cls.spmd_typecheck
|
|
757
|
+
names = tuple(inspect.signature(hook).parameters)[1:]
|
|
758
|
+
hook(outputs, **{name: forward_args[name] for name in names})
|
|
686
759
|
|
|
687
|
-
|
|
688
|
-
their outputs untyped (or raise in strict mode), since the checker
|
|
689
|
-
cannot know whether the function is safe for automatic type propagation.
|
|
760
|
+
return outputs
|
|
690
761
|
|
|
691
|
-
Can be used as a decorator::
|
|
692
762
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return
|
|
763
|
+
def _get_autograd_spmd_typecheck(cls: type) -> Callable[..., object] | None:
|
|
764
|
+
"""Return and validate an autograd.Function's SPMD typecheck hook."""
|
|
765
|
+
hook = getattr(cls, "spmd_typecheck", None)
|
|
766
|
+
if hook is not None and not callable(hook):
|
|
767
|
+
raise TypeError(f"{cls.__name__}.spmd_typecheck must be callable")
|
|
768
|
+
return hook
|
|
699
769
|
|
|
700
770
|
|
|
701
771
|
def register_autograd_function(cls: type) -> type:
|
|
@@ -705,34 +775,44 @@ def register_autograd_function(cls: type) -> type:
|
|
|
705
775
|
non-trivial type transformations where the default local-only rule
|
|
706
776
|
would produce incorrect output types.
|
|
707
777
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
``typecheck_forward`` on ``nn.Module`` subclasses in llama4x::
|
|
778
|
+
A class may define an ``spmd_typecheck`` staticmethod. Its presence is
|
|
779
|
+
detected automatically, so new classes do not need this decorator. The
|
|
780
|
+
hook runs after the function, receives its exact return value as the first
|
|
781
|
+
argument, and names only the forward arguments it needs::
|
|
713
782
|
|
|
714
|
-
@register_autograd_function
|
|
715
783
|
class MyCollectiveOp(torch.autograd.Function):
|
|
716
784
|
@staticmethod
|
|
717
785
|
def forward(ctx, x, y):
|
|
718
786
|
return x + y
|
|
719
787
|
|
|
720
788
|
@staticmethod
|
|
721
|
-
def
|
|
789
|
+
def spmd_typecheck(outputs, *, x):
|
|
722
790
|
assert_type(x, {pg: S(-1)})
|
|
723
|
-
|
|
724
|
-
assert_type(out, {pg: R})
|
|
725
|
-
return out
|
|
791
|
+
assert_type_like(outputs, x, {pg: R})
|
|
726
792
|
|
|
727
793
|
@staticmethod
|
|
728
794
|
def backward(ctx, g):
|
|
729
795
|
return g, g
|
|
796
|
+
|
|
797
|
+
The legacy ``typecheck_forward`` wrapper remains supported for existing
|
|
798
|
+
registrations, but cannot be combined with ``spmd_typecheck``.
|
|
730
799
|
"""
|
|
731
|
-
|
|
800
|
+
legacy_hook = getattr(cls, "typecheck_forward", None)
|
|
801
|
+
spmd_typecheck = _get_autograd_spmd_typecheck(cls)
|
|
802
|
+
|
|
803
|
+
if legacy_hook is not None and spmd_typecheck is not None:
|
|
804
|
+
raise TypeError(
|
|
805
|
+
f"{cls.__name__} cannot define typecheck_forward together with "
|
|
806
|
+
"spmd_typecheck"
|
|
807
|
+
)
|
|
808
|
+
if legacy_hook is None and spmd_typecheck is None:
|
|
732
809
|
raise TypeError(
|
|
733
|
-
f"{cls.__name__} must define
|
|
810
|
+
f"{cls.__name__} must define typecheck_forward or spmd_typecheck "
|
|
734
811
|
f"when using @register_autograd_function"
|
|
735
812
|
)
|
|
813
|
+
if legacy_hook is not None and not callable(legacy_hook):
|
|
814
|
+
raise TypeError(f"{cls.__name__}.typecheck_forward must be callable")
|
|
815
|
+
|
|
736
816
|
_TYPECHECK_AUTOGRAD_FUNCTIONS.add(cls)
|
|
737
817
|
return cls
|
|
738
818
|
|
|
@@ -908,7 +988,32 @@ def local():
|
|
|
908
988
|
yield
|
|
909
989
|
|
|
910
990
|
|
|
911
|
-
|
|
991
|
+
@overload
|
|
992
|
+
def no_typecheck() -> AbstractContextManager[None]: ...
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
@overload
|
|
996
|
+
def no_typecheck(
|
|
997
|
+
*, out_types: Any, in_types: Any = ...
|
|
998
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def no_typecheck(**kwargs: Any):
|
|
1002
|
+
"""Disable checking in a context or across an explicitly typed function."""
|
|
1003
|
+
if not kwargs:
|
|
1004
|
+
return _no_typecheck_context()
|
|
1005
|
+
if "out_types" not in kwargs or kwargs.keys() - {"in_types", "out_types"}:
|
|
1006
|
+
raise TypeError("out_types is required for typed no_typecheck")
|
|
1007
|
+
|
|
1008
|
+
return _typecheck_boundary(
|
|
1009
|
+
name="no_typecheck",
|
|
1010
|
+
body_context=_no_typecheck_context,
|
|
1011
|
+
in_types=kwargs.get("in_types", Infer),
|
|
1012
|
+
out_types=kwargs["out_types"],
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def local_map(
|
|
912
1017
|
*,
|
|
913
1018
|
in_types: Any = Infer,
|
|
914
1019
|
out_types: Any,
|
|
@@ -980,12 +1085,23 @@ def local_map( # noqa: C901
|
|
|
980
1085
|
def fn(x, weights):
|
|
981
1086
|
return x @ weights[0], x @ weights[1]
|
|
982
1087
|
"""
|
|
1088
|
+
return _typecheck_boundary(
|
|
1089
|
+
name="local_map",
|
|
1090
|
+
body_context=local,
|
|
1091
|
+
in_types=in_types,
|
|
1092
|
+
out_types=out_types,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _typecheck_boundary( # noqa: C901
|
|
1097
|
+
*, name: str, body_context: Any, in_types: Any, out_types: Any
|
|
1098
|
+
):
|
|
983
1099
|
import functools
|
|
984
1100
|
|
|
985
1101
|
from torch.utils._pytree import tree_map
|
|
986
1102
|
|
|
987
1103
|
def decorator(fn):
|
|
988
|
-
prefix = f"
|
|
1104
|
+
prefix = f"{name}[{fn.__qualname__}]"
|
|
989
1105
|
|
|
990
1106
|
def _check_arg(arg: object, spec: _LocalMapSpecLeaf, kind: str) -> None:
|
|
991
1107
|
if spec is None:
|
|
@@ -1087,7 +1203,7 @@ def local_map( # noqa: C901
|
|
|
1087
1203
|
if in_types is not Infer:
|
|
1088
1204
|
_walk_boundary(args, in_types, "input")
|
|
1089
1205
|
|
|
1090
|
-
with
|
|
1206
|
+
with body_context():
|
|
1091
1207
|
result = fn(*args, **kwargs)
|
|
1092
1208
|
|
|
1093
1209
|
_walk_boundary(result, out_types, "output")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spmd_types
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: A type system for distributed (SPMD) tensor computations in PyTorch
|
|
5
5
|
Author: Meta Platforms, Inc.
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -47,7 +47,6 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
47
47
|
Requires-Python: >=3.10
|
|
48
48
|
Description-Content-Type: text/markdown
|
|
49
49
|
License-File: LICENSE
|
|
50
|
-
Requires-Dist: torch>=2.10.0
|
|
51
50
|
Dynamic: license-file
|
|
52
51
|
|
|
53
52
|
# `spmd_types`
|
|
@@ -10,6 +10,7 @@ spmd_types/_dtensor_checker.py
|
|
|
10
10
|
spmd_types/_dtype_utils.py
|
|
11
11
|
spmd_types/_frame.py
|
|
12
12
|
spmd_types/_local.py
|
|
13
|
+
spmd_types/_local_registration.py
|
|
13
14
|
spmd_types/_mesh.py
|
|
14
15
|
spmd_types/_mesh_axis.py
|
|
15
16
|
spmd_types/_mesh_region.py
|
|
@@ -29,6 +30,5 @@ spmd_types/types.py
|
|
|
29
30
|
spmd_types.egg-info/PKG-INFO
|
|
30
31
|
spmd_types.egg-info/SOURCES.txt
|
|
31
32
|
spmd_types.egg-info/dependency_links.txt
|
|
32
|
-
spmd_types.egg-info/requires.txt
|
|
33
33
|
spmd_types.egg-info/top_level.txt
|
|
34
34
|
spmd_types/_checker/__init__.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
torch>=2.10.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|