spmd_types 0.2.2__tar.gz → 0.2.3__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.
Files changed (36) hide show
  1. {spmd_types-0.2.2/spmd_types.egg-info → spmd_types-0.2.3}/PKG-INFO +1 -2
  2. {spmd_types-0.2.2 → spmd_types-0.2.3}/pyproject.toml +1 -2
  3. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/__init__.py +3 -1
  4. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_checker/__init__.py +61 -14
  5. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_mesh.py +95 -10
  6. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_state.py +24 -14
  7. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/checker.py +2 -2
  8. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/runtime.py +147 -39
  9. {spmd_types-0.2.2 → spmd_types-0.2.3/spmd_types.egg-info}/PKG-INFO +1 -2
  10. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types.egg-info/SOURCES.txt +0 -1
  11. spmd_types-0.2.2/spmd_types.egg-info/requires.txt +0 -1
  12. {spmd_types-0.2.2 → spmd_types-0.2.3}/LICENSE +0 -0
  13. {spmd_types-0.2.2 → spmd_types-0.2.3}/README.md +0 -0
  14. {spmd_types-0.2.2 → spmd_types-0.2.3}/setup.cfg +0 -0
  15. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_backward_hooks.py +0 -0
  16. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_collectives.py +0 -0
  17. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_dist.py +0 -0
  18. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_dtensor.py +0 -0
  19. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_dtensor_checker.py +0 -0
  20. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_dtype_utils.py +0 -0
  21. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_frame.py +0 -0
  22. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_local.py +0 -0
  23. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_mesh_axis.py +0 -0
  24. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_mesh_region.py +0 -0
  25. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_raw_dist.py +0 -0
  26. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_reinterpret_mesh.py +0 -0
  27. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_scalar.py +0 -0
  28. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_scalar_sentinel.py +0 -0
  29. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_test_utils.py +0 -0
  30. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_testing.py +0 -0
  31. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_traceback.py +0 -0
  32. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_type_attr.py +0 -0
  33. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/_vmap.py +0 -0
  34. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types/types.py +0 -0
  35. {spmd_types-0.2.2 → spmd_types-0.2.3}/spmd_types.egg-info/dependency_links.txt +0 -0
  36. {spmd_types-0.2.2 → spmd_types-0.2.3}/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.2
3
+ Version: 0.2.3
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.2"
7
+ version = "0.2.3"
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]
@@ -41,7 +41,6 @@ from spmd_types._state import ( # noqa: F401
41
41
  current_mesh_all_names,
42
42
  current_mesh_names,
43
43
  is_type_checking,
44
- no_typecheck,
45
44
  )
46
45
  from spmd_types._traceback import traceback_filtering # noqa: F401
47
46
  from spmd_types._type_attr import ( # noqa: F401
@@ -56,10 +55,13 @@ from spmd_types.runtime import ( # noqa: F401
56
55
  assert_local_type_like,
57
56
  assert_type,
58
57
  assert_type_like,
58
+ get_partition_spec,
59
+ has_local_type,
59
60
  Infer,
60
61
  local,
61
62
  local_map,
62
63
  mutate_type,
64
+ no_typecheck,
63
65
  register_autograd_function,
64
66
  register_decomposition,
65
67
  register_local_autograd_function,
@@ -68,6 +68,7 @@ from spmd_types.runtime import ( # noqa: F401
68
68
  get_partition_spec,
69
69
  has_local_type,
70
70
  mutate_type,
71
+ no_typecheck,
71
72
  register_autograd_function,
72
73
  register_decomposition,
73
74
  register_local_autograd_function,
@@ -2531,6 +2532,8 @@ def _get_autograd_function_class(func) -> type | None:
2531
2532
  def _validate_partition_spec_for_global_spmd(
2532
2533
  local_type: LocalSpmdType,
2533
2534
  spec: PartitionSpec | None,
2535
+ *,
2536
+ is_global_axis: Callable[[MeshAxis], bool],
2534
2537
  ) -> None:
2535
2538
  """Validate PartitionSpec <-> local type consistency.
2536
2539
 
@@ -2543,10 +2546,12 @@ def _validate_partition_spec_for_global_spmd(
2543
2546
  Args:
2544
2547
  local_type: The tensor's local SPMD type dict.
2545
2548
  spec: The tensor's PartitionSpec (None if absent).
2549
+ is_global_axis: Returns whether an axis requires global PartitionSpec
2550
+ validation in the current mesh context.
2546
2551
  """
2547
2552
  spec_axes = spec.axes_with_partition_spec() if spec is not None else set()
2548
2553
  for axis, typ in local_type.items():
2549
- if typ is V and axis not in spec_axes:
2554
+ if is_global_axis(axis) and typ is V and axis not in spec_axes:
2550
2555
  ax = format_axis(axis)
2551
2556
  raise SpmdTypeError(
2552
2557
  f"Tensor has Varying type on axis {ax} but no PartitionSpec entry. "
@@ -2555,7 +2560,7 @@ def _validate_partition_spec_for_global_spmd(
2555
2560
  )
2556
2561
  for axis in spec_axes:
2557
2562
  typ = local_type.get(axis)
2558
- if typ is not V:
2563
+ if is_global_axis(axis) and typ is not V:
2559
2564
  ax = format_axis(axis)
2560
2565
  raise SpmdTypeError(
2561
2566
  f"Tensor has PartitionSpec on axis {ax} but local type is "
@@ -2623,6 +2628,23 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
2623
2628
 
2624
2629
  self._disabled = False
2625
2630
 
2631
+ def _is_global_axis(self, axis: MeshAxis) -> bool:
2632
+ if self._local:
2633
+ return False
2634
+ local_axes = _state._current_local_axes()
2635
+ if not local_axes:
2636
+ return True
2637
+ mesh = _state.current_mesh()
2638
+ if mesh is None or axis not in mesh:
2639
+ raise SpmdTypeError(
2640
+ f"Axis {format_axis(axis)} is not in the current mesh, so its "
2641
+ "local/global typechecking policy is undefined"
2642
+ )
2643
+ return axis not in local_axes
2644
+
2645
+ def _global_axes_for(self, axes: Iterable[MeshAxis]) -> set[MeshAxis]:
2646
+ return {axis for axis in axes if self._is_global_axis(axis)}
2647
+
2626
2648
  def __torch_function__(self, func, types, args=(), kwargs=None): # noqa: C901
2627
2649
  kwargs = kwargs or {}
2628
2650
  # Paused via no_typecheck(): run without type checking.
@@ -2679,7 +2701,11 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
2679
2701
  # Global SPMD validation.
2680
2702
  if not self._local:
2681
2703
  for local_type, spec in zip(info.tensor_types, info.partition_specs):
2682
- _validate_partition_spec_for_global_spmd(local_type, spec)
2704
+ _validate_partition_spec_for_global_spmd(
2705
+ local_type,
2706
+ spec,
2707
+ is_global_axis=self._is_global_axis,
2708
+ )
2683
2709
 
2684
2710
  # Typecheck-registered autograd function: dispatch to typecheck_forward
2685
2711
  # INSTEAD of executing func. The mode is popped off the
@@ -2744,6 +2770,29 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
2744
2770
  # Resolve axis to a list of matching axes on the tensor.
2745
2771
  # Direct match: [axis]. Flattened decomposition: [sub1, sub2, ...].
2746
2772
  resolved_axes = _resolve_collective_axes(axis, local_type)
2773
+ collective_axes = resolved_axes or [axis]
2774
+ global_collective_axes = [
2775
+ ax for ax in collective_axes if self._is_global_axis(ax)
2776
+ ]
2777
+ if global_collective_axes and len(global_collective_axes) != len(
2778
+ collective_axes
2779
+ ):
2780
+ global_axis_set = set(global_collective_axes)
2781
+ local_collective_axes = [
2782
+ ax for ax in collective_axes if ax not in global_axis_set
2783
+ ]
2784
+ local_names = ", ".join(
2785
+ format_axis(ax) for ax in local_collective_axes
2786
+ )
2787
+ global_names = ", ".join(
2788
+ format_axis(ax) for ax in global_collective_axes
2789
+ )
2790
+ raise SpmdTypeError(
2791
+ f"Collective axis {format_axis(axis)} spans mesh axes with "
2792
+ f"mixed typechecking policies: local axes ({local_names}) "
2793
+ f"and global axes ({global_names}). Pass a collective axis "
2794
+ f"whose component axes are all local or all global."
2795
+ )
2747
2796
 
2748
2797
  if resolved_axes is not None:
2749
2798
  # Validate input types on all resolved axes
@@ -2774,7 +2823,7 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
2774
2823
  output_type[ax] = local_dst
2775
2824
  _set_local_type(result, output_type)
2776
2825
 
2777
- if _state._is_global():
2826
+ if global_collective_axes:
2778
2827
  input_spec = get_partition_spec(x)
2779
2828
  # Find S(dim) for this axis in the input spec, validating
2780
2829
  # it is innermost in its multi-axis group.
@@ -2916,15 +2965,16 @@ class _SpmdTypeMode(torch.overrides.TorchFunctionMode):
2916
2965
  )
2917
2966
 
2918
2967
  # Stage 3: global shard propagation.
2919
- if self._local:
2920
- global_shard_axes, shard_edges = [], {}
2921
- else:
2922
- all_axes: set[DeviceMeshAxis] = set()
2923
- for typ in input_types_list:
2924
- all_axes.update(typ.keys())
2968
+ all_axes: set[MeshAxis] = set()
2969
+ for typ in input_types_list:
2970
+ all_axes.update(typ.keys())
2971
+ global_axes = self._global_axes_for(all_axes)
2972
+ if global_axes:
2925
2973
  global_shard_axes, shard_edges = _collect_shard_axes(
2926
- partition_specs, all_axes
2974
+ partition_specs, global_axes
2927
2975
  )
2976
+ else:
2977
+ global_shard_axes, shard_edges = [], {}
2928
2978
 
2929
2979
  # Validate mutation safety for in-place/out operations.
2930
2980
  mutated = _get_mutated_tensors(func, args, kwargs, result)
@@ -3086,9 +3136,6 @@ def typecheck(
3086
3136
  _remove_autograd_apply_patch()
3087
3137
 
3088
3138
 
3089
- from spmd_types._state import no_typecheck # noqa: F401
3090
-
3091
-
3092
3139
  class _SpmdTypeBackwardCompatibleMode:
3093
3140
  """Backwards-compatible wrapper around ``typecheck()``.
3094
3141
 
@@ -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] | DeviceMesh | Sequence[ProcessGroup] | dict[str, 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
- resolved, names = _resolve_axes(axes)
46
- _push_mesh(resolved, names, _axes_to_pgs(axes))
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
- stack = getattr(_tls, "mesh_stack", None)
67
- pgs = stack[-1].pgs if stack else None
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 = (current_mesh_all_names() or {}).get(axis)
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 no_typecheck():
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
- stack = getattr(_tls, "mesh_stack", None)
81
- if stack:
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
- stack = getattr(_tls, "mesh_stack", None)
93
- if stack:
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
- stack = getattr(_tls, "mesh_stack", None)
101
- if stack:
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(MeshEntry(resolved, filtered_names, all_names, resolved_pgs))
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, no_typecheck # noqa: F401
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
@@ -27,14 +27,14 @@ from __future__ import annotations
27
27
  import builtins
28
28
  import logging
29
29
  import os
30
- from contextlib import contextmanager
31
- from typing import Any, TypeAlias
30
+ from contextlib import AbstractContextManager, contextmanager
31
+ from typing import Any, Callable, overload, TypeAlias
32
32
 
33
33
  import torch
34
34
  from spmd_types._frame import _get_user_frame
35
35
  from spmd_types._mesh_axis import MeshAxis
36
36
  from spmd_types._scalar_sentinel import _Scalar
37
- from spmd_types._state import current_mesh, is_type_checking
37
+ from spmd_types._state import _no_typecheck_context, current_mesh, is_type_checking
38
38
  from spmd_types._traceback import api_boundary
39
39
  from spmd_types._type_attr import (
40
40
  _LOCAL_TYPE_ATTR,
@@ -589,20 +589,49 @@ def assert_local_type_like(
589
589
  # =============================================================================
590
590
 
591
591
 
592
- @api_boundary
593
- def mutate_type(
594
- tensor: torch.Tensor,
592
+ def _mutate_type_single_axis(
593
+ local_type: LocalSpmdType,
595
594
  axis: DeviceMeshAxis,
596
595
  *,
597
596
  src: PerMeshAxisSpmdType,
598
597
  dst: PerMeshAxisSpmdType,
598
+ ) -> LocalSpmdType:
599
+ axis = normalize_axis(axis)
600
+ if axis.size() == 1:
601
+ return local_type
602
+ if axis not in local_type:
603
+ raise SpmdTypeError(
604
+ f"mutate_type: axis {format_axis(axis)} not found in tensor's "
605
+ f"SPMD type. Tensor has axes: {set(local_type.keys())}"
606
+ )
607
+
608
+ src_local = to_local_type(src)
609
+ current = local_type[axis]
610
+ if current is not src_local:
611
+ raise SpmdTypeError(
612
+ f"mutate_type: expected current type {src_local} on axis "
613
+ f"{format_axis(axis)}, got {current}"
614
+ )
615
+
616
+ new_type = dict(local_type)
617
+ new_type[axis] = to_local_type(dst)
618
+ return new_type
619
+
620
+
621
+ @api_boundary
622
+ def mutate_type(
623
+ tensor: torch.Tensor,
624
+ axis: DeviceMeshAxis | None = None,
625
+ *,
626
+ src: PerMeshAxisSpmdType | dict[DeviceMeshAxis, PerMeshAxisSpmdType],
627
+ dst: PerMeshAxisSpmdType | dict[DeviceMeshAxis, PerMeshAxisSpmdType],
599
628
  ) -> torch.Tensor:
600
- """Change the SPMD type of a single mesh axis on an already-annotated tensor.
629
+ """Change the SPMD type of mesh axes on an already-annotated tensor.
601
630
 
602
631
  Unlike ``assert_type``, this function *overwrites* the existing type on
603
- ``axis``. The caller must specify the expected current type (``src``) to
604
- prevent silent corruption; a ``SpmdTypeError`` is raised if the tensor's
605
- current type on ``axis`` does not match ``src``.
632
+ the updated axes. The caller must specify the expected current type
633
+ (``src``) to prevent silent corruption; a ``SpmdTypeError`` is raised if
634
+ an updated axis's current type does not match its expected ``src``.
606
635
 
607
636
  This is intended for internal use in low-level parallelism primitives
608
637
  where a buffer legitimately changes its distribution semantics in place
@@ -613,45 +642,88 @@ def mutate_type(
613
642
  *views* into a buffer after an in-place collective has changed the
614
643
  buffer's semantics.
615
644
 
645
+ There are four supported call shapes:
646
+
647
+ - ``mutate_type(tensor, axis, src=R, dst=I)`` updates a single axis.
648
+ - ``mutate_type(tensor, src=R, dst=I)`` updates all axes annotated on the
649
+ tensor, assuming all axes share ``src`` type.
650
+ - ``mutate_type(tensor, src=R, dst={axis0: I, axis1: P})`` updates every
651
+ axis in ``dst``, after checking all update axes have ``src`` type.
652
+ Axes not listed in ``dst`` are unaffected.
653
+ - ``mutate_type(tensor, src={axis0: R, axis1: I}, dst=P)`` is similarly supported.
654
+ - ``mutate_type(tensor, src={axis0: R, axis1: R}, dst={axis0: I, axis1: I})``
655
+ updates each axis correspondingly. ``src`` and ``dst`` keys must match,
656
+ and other axes are unaffected.
657
+
658
+ ``mutate_type`` errors if any specified ``src`` type doesn't match the tensor's
659
+ annotation.
660
+
616
661
  Args:
617
662
  tensor: The tensor whose type to mutate. Must already have a local
618
663
  type annotation.
619
- axis: The mesh axis (MeshAxis or ProcessGroup) to modify.
620
- src: The expected current type on ``axis``. Raises if it does not
621
- match. Accepts R, I, V, P, or S(i) (S(i) is compared as V).
622
- dst: The new type to set on ``axis``. Accepts R, I, V, P, or S(i)
623
- (S(i) is stored as V).
664
+ axis: The mesh axis (MeshAxis or ProcessGroup) to modify for the
665
+ single-axis form. Omit when ``src`` or ``dst`` is a dict mapping
666
+ axes to types.
667
+ src: The expected current type, or a dict of per-axis expected current
668
+ types. Accepts R, I, V, P, or S(i); S(i) is compared as V.
669
+ dst: The new type, or a dict of per-axis destination types. Accepts R,
670
+ I, V, P, or S(i); S(i) is stored as V.
624
671
 
625
672
  Returns:
626
673
  The tensor for chaining.
627
674
 
628
675
  Raises:
629
- SpmdTypeError: If the tensor has no type, the axis is missing,
630
- or the current type does not match ``src``.
676
+ SpmdTypeError: If the tensor has no type, if a non-singleton updated
677
+ axis is missing from the tensor's local type, if a current type
678
+ does not match ``src``, or if per-axis ``src`` and ``dst`` dict keys
679
+ differ.
631
680
  """
632
- axis = normalize_axis(axis)
633
- if axis.size() == 1:
634
- return tensor # singleton axes are not tracked
635
681
  local_type = get_local_type(tensor)
636
- if axis not in local_type:
637
- raise SpmdTypeError(
638
- f"mutate_type: axis {format_axis(axis)} not found in tensor's "
639
- f"SPMD type. Tensor has axes: {set(local_type.keys())}"
640
- )
641
-
642
- # Normalize S(i) -> V for comparison and storage
643
- src_local = to_local_type(src)
644
- dst_local = to_local_type(dst)
645
-
646
- current = local_type[axis]
647
- if current is not src_local:
648
- raise SpmdTypeError(
649
- f"mutate_type: expected current type {src_local} on axis "
650
- f"{format_axis(axis)}, got {current}"
682
+ if isinstance(dst, dict) and isinstance(src, dict):
683
+ if axis is not None:
684
+ raise SpmdTypeError(
685
+ "mutate_type() expected axis to be omitted when src or dst is a dict."
686
+ )
687
+ if set(dst) != set(src):
688
+ axes = ", ".join(format_axis(axis) for axis in set(dst) ^ set(src))
689
+ raise SpmdTypeError(
690
+ f"mutate_type() expected src and dst axes to match: {axes}"
691
+ )
692
+ updates = [
693
+ (update_axis, src[update_axis], dst_type)
694
+ for update_axis, dst_type in dst.items()
695
+ ]
696
+ elif isinstance(dst, dict):
697
+ if axis is not None:
698
+ raise SpmdTypeError(
699
+ "mutate_type() expected axis to be omitted when src or dst is a dict."
700
+ )
701
+ updates = [
702
+ (update_axis, src, dst_type) for update_axis, dst_type in dst.items()
703
+ ]
704
+ elif isinstance(src, dict):
705
+ if axis is not None:
706
+ raise SpmdTypeError(
707
+ "mutate_type() expected axis to be omitted when src or dst is a dict."
708
+ )
709
+ updates = [
710
+ (update_axis, src_type, dst) for update_axis, src_type in src.items()
711
+ ]
712
+ else:
713
+ updates = (
714
+ [(axis, src, dst)]
715
+ if axis is not None
716
+ else [(update_axis, src, dst) for update_axis in local_type.keys()]
651
717
  )
652
718
 
653
719
  new_type = dict(local_type)
654
- new_type[axis] = dst_local
720
+ for update_axis, src_type, dst_type in updates:
721
+ new_type = _mutate_type_single_axis(
722
+ new_type,
723
+ update_axis,
724
+ src=src_type,
725
+ dst=dst_type,
726
+ )
655
727
  return _set_local_type(tensor, new_type)
656
728
 
657
729
 
@@ -908,7 +980,32 @@ def local():
908
980
  yield
909
981
 
910
982
 
911
- def local_map( # noqa: C901
983
+ @overload
984
+ def no_typecheck() -> AbstractContextManager[None]: ...
985
+
986
+
987
+ @overload
988
+ def no_typecheck(
989
+ *, out_types: Any, in_types: Any = ...
990
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: ...
991
+
992
+
993
+ def no_typecheck(**kwargs: Any):
994
+ """Disable checking in a context or across an explicitly typed function."""
995
+ if not kwargs:
996
+ return _no_typecheck_context()
997
+ if "out_types" not in kwargs or kwargs.keys() - {"in_types", "out_types"}:
998
+ raise TypeError("out_types is required for typed no_typecheck")
999
+
1000
+ return _typecheck_boundary(
1001
+ name="no_typecheck",
1002
+ body_context=_no_typecheck_context,
1003
+ in_types=kwargs.get("in_types", Infer),
1004
+ out_types=kwargs["out_types"],
1005
+ )
1006
+
1007
+
1008
+ def local_map(
912
1009
  *,
913
1010
  in_types: Any = Infer,
914
1011
  out_types: Any,
@@ -980,12 +1077,23 @@ def local_map( # noqa: C901
980
1077
  def fn(x, weights):
981
1078
  return x @ weights[0], x @ weights[1]
982
1079
  """
1080
+ return _typecheck_boundary(
1081
+ name="local_map",
1082
+ body_context=local,
1083
+ in_types=in_types,
1084
+ out_types=out_types,
1085
+ )
1086
+
1087
+
1088
+ def _typecheck_boundary( # noqa: C901
1089
+ *, name: str, body_context: Any, in_types: Any, out_types: Any
1090
+ ):
983
1091
  import functools
984
1092
 
985
1093
  from torch.utils._pytree import tree_map
986
1094
 
987
1095
  def decorator(fn):
988
- prefix = f"local_map[{fn.__qualname__}]"
1096
+ prefix = f"{name}[{fn.__qualname__}]"
989
1097
 
990
1098
  def _check_arg(arg: object, spec: _LocalMapSpecLeaf, kind: str) -> None:
991
1099
  if spec is None:
@@ -1087,7 +1195,7 @@ def local_map( # noqa: C901
1087
1195
  if in_types is not Infer:
1088
1196
  _walk_boundary(args, in_types, "input")
1089
1197
 
1090
- with local():
1198
+ with body_context():
1091
1199
  result = fn(*args, **kwargs)
1092
1200
 
1093
1201
  _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.2
3
+ Version: 0.2.3
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`
@@ -29,6 +29,5 @@ spmd_types/types.py
29
29
  spmd_types.egg-info/PKG-INFO
30
30
  spmd_types.egg-info/SOURCES.txt
31
31
  spmd_types.egg-info/dependency_links.txt
32
- spmd_types.egg-info/requires.txt
33
32
  spmd_types.egg-info/top_level.txt
34
33
  spmd_types/_checker/__init__.py
@@ -1 +0,0 @@
1
- torch>=2.10.0
File without changes
File without changes
File without changes