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.
Files changed (37) hide show
  1. {spmd_types-0.2.2/spmd_types.egg-info → spmd_types-0.2.4}/PKG-INFO +1 -2
  2. {spmd_types-0.2.2 → spmd_types-0.2.4}/pyproject.toml +1 -2
  3. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/__init__.py +7 -5
  4. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_backward_hooks.py +4 -16
  5. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_checker/__init__.py +87 -34
  6. spmd_types-0.2.4/spmd_types/_local_registration.py +50 -0
  7. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_mesh.py +95 -10
  8. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_state.py +24 -14
  9. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/checker.py +2 -2
  10. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/runtime.py +197 -81
  11. {spmd_types-0.2.2 → spmd_types-0.2.4/spmd_types.egg-info}/PKG-INFO +1 -2
  12. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types.egg-info/SOURCES.txt +1 -1
  13. spmd_types-0.2.2/spmd_types.egg-info/requires.txt +0 -1
  14. {spmd_types-0.2.2 → spmd_types-0.2.4}/LICENSE +0 -0
  15. {spmd_types-0.2.2 → spmd_types-0.2.4}/README.md +0 -0
  16. {spmd_types-0.2.2 → spmd_types-0.2.4}/setup.cfg +0 -0
  17. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_collectives.py +0 -0
  18. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dist.py +0 -0
  19. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dtensor.py +0 -0
  20. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dtensor_checker.py +0 -0
  21. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_dtype_utils.py +0 -0
  22. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_frame.py +0 -0
  23. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_local.py +0 -0
  24. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_mesh_axis.py +0 -0
  25. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_mesh_region.py +0 -0
  26. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_raw_dist.py +0 -0
  27. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_reinterpret_mesh.py +0 -0
  28. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_scalar.py +0 -0
  29. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_scalar_sentinel.py +0 -0
  30. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_test_utils.py +0 -0
  31. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_testing.py +0 -0
  32. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_traceback.py +0 -0
  33. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_type_attr.py +0 -0
  34. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/_vmap.py +0 -0
  35. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types/types.py +0 -0
  36. {spmd_types-0.2.2 → spmd_types-0.2.4}/spmd_types.egg-info/dependency_links.txt +0 -0
  37. {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.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.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
- _LOCAL_AUTOGRAD_FUNCTIONS,
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 a typecheck_forward
2458
- # staticmethod. When .apply() is intercepted by __torch_function__,
2459
- # typecheck_forward is called INSTEAD of .apply(). It should assert_type()
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(local_type, spec)
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: dispatch to typecheck_forward
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 .apply() called
2688
- # inside typecheck_forward executes normally without re-entering
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
- autograd_cls is not None
2693
- and autograd_cls in _TYPECHECK_AUTOGRAD_FUNCTIONS
2694
- ):
2695
- return autograd_cls.typecheck_forward(*args, **kwargs)
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 _state._is_global():
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, and provide "
2863
- f"register_autograd_function("
2864
- f"{autograd_cls.__name__}) for a custom "
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
- 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())
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, all_axes
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] | 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
@@ -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
- @api_boundary
593
- def mutate_type(
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 a single mesh axis on an already-annotated tensor.
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
- ``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``.
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
- 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).
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, the axis is missing,
630
- or the current type does not match ``src``.
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 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}"
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
- new_type[axis] = dst_local
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
- # Sets of autograd.Function subclasses registered for type checking.
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 register_local_autograd_function(cls: type) -> type:
670
- """Register an autograd.Function subclass as local-only for SPMD type checking.
671
-
672
- Local-only means the function's forward operates element-wise (or more
673
- generally, does not rearrange data across the tensor in a way that would
674
- change its sharding type). It must NOT perform any collectives or
675
- cross-device communication. For functions that do, use
676
- :func:`register_autograd_function` with a ``typecheck_forward`` method instead.
677
-
678
- Registered functions get the standard local type propagation rule when
679
- type checking is active:
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
- - Inputs may freely mix R and V types; the output is R unless any input
682
- is V, in which case it is V.
683
- - All-I inputs produce I outputs.
684
- - R/V and I cannot be mixed.
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
- Unregistered autograd functions that reach the type checker will leave
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
- @register_local_autograd_function
694
- class MyOp(torch.autograd.Function):
695
- ...
696
- """
697
- _LOCAL_AUTOGRAD_FUNCTIONS.add(cls)
698
- return cls
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
- The class must define a ``typecheck_forward`` staticmethod that receives
709
- the same positional/keyword arguments as ``.apply()``. Inside, it should
710
- call ``assert_type`` on inputs, call ``.apply()`` to run the function,
711
- then call ``assert_type`` on the output. This is symmetric with
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 typecheck_forward(x, y):
789
+ def spmd_typecheck(outputs, *, x):
722
790
  assert_type(x, {pg: S(-1)})
723
- out = MyCollectiveOp.apply(x, y)
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
- if not callable(getattr(cls, "typecheck_forward", None)):
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 a typecheck_forward staticmethod "
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
- def local_map( # noqa: C901
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"local_map[{fn.__qualname__}]"
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 local():
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.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