Skip to content

Commit 927e7f7

Browse files
authored
chore: update contribution guidelines
1 parent 244b368 commit 927e7f7

203 files changed

Lines changed: 1082 additions & 2030 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ We provide a `Makefile` to automate common development tasks.
6565

6666
We enforce strict quality standards to keep the codebase maintainable.
6767

68+
### Design Principles
69+
70+
They are not enforced by tooling, but PRs that violate them may be asked to change.
71+
72+
* **Composition over inheritance**: components are small single-responsibility classes wired together (see `d9d/loop/component/`). Avoid "God classes" that accumulate unrelated responsibilities, avoid speculative base classes that exist only to hoard "common" code.
73+
* **Define contracts structurally.** Use a `typing.Protocol` for a *trait* - a secondary capability bolted onto a type that already has its own base class (e.g. `ModuleLateInit` on an `nn.Module`), where you want duck-typed conformance without forcing inheritance. Use an `abc.ABC` when the interface *is* the object's primary identity and the hierarchy is the "main" type (e.g. `PipelineSchedule`).
74+
* **No reflection where it can be avoided.** Avoid `getattr` / `hasattr` / `inspect` and string-name dispatch. Prefer an explicit `match`-`case`, a proper interface, or a factory. Reflection is acceptable *only* when introspection is intrinsic to the feature itself - i.e. declarative registration APIs that cannot work without it, such as a `@subscribe`/`@register` decorator wiring handlers by signature.
75+
* **Inject dependencies; don't reach for them.** Components receive their collaborators as constructor arguments and store them as private fields. Don't pull them from globals/singletons or construct them internally - wiring happens at the edges (`d9d/loop/run/`).
76+
* **Reuse before reinventing.** If PyTorch or the stdlib already solves it, use it, rather than hand-rolling an equivalent.
77+
* **No needless indirection.** Don't add a wrapper that only forwards to another function/object without adding meaning. Inline it instead.
78+
* **Validate eagerly, fail fast.** Validate constructor args up front; raise if a method is called outside its required lifecycle scope rather than silently misbehaving.
79+
* **Decide behavior from explicit inputs, not inferred state.** Drive branching with an explicit parameter, not by sniffing the shape/dtype/contents of the data. Inferred checks silently encode invariants the caller and the next reader won't know are there - make them part of the signature instead.
80+
* **Validate at the boundary; trust within it**. Data crossing an untrusted boundary (user config, deserialized state) is validated once at the edge into a model that guarantees its own invariants — that's the validation layer, and we use `pydantic` for it. Pass trusted internal data as plain `dataclasses` and assume it is already valid. Don't re-validate trusted internal data, and don't pass unvalidated raw input deeper than the edge.
81+
* **Separate configuration from behavior.** Config objects describe; classes behave. Don't merge them into one dataclass that needs `__post_init__` magic.
82+
* **Polymorphism for configurable objects via discriminated unions.** When a configurable object has selectable behavior, model the choices as a Pydantic discriminated union and resolve them in a `build_*()` factory with an exhaustive `match (case _: raise)`.
83+
6884
### Linting & Formatting
6985
We use [Ruff](https://docs.astral.sh/ruff/) for both linting and formatting.
7086
Configuration is strict (see `pyproject.toml` for enabled rules).
@@ -84,6 +100,15 @@ We have two tiers of tests:
84100
85101
**Requirement:** All PRs must pass `make test`. If you add a feature, you must add corresponding tests.
86102
103+
### Docstrings
104+
105+
We follow the [Google Python style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
106+
107+
* **Style:** Use Google-style docstrings (`Args:`, `Returns:`, `Raises:`, etc.).
108+
* **No type annotations in docstrings:** Types are already declared in the signature and checked by `ty`. Do not repeat them in the docstring.
109+
* **Document `__init__`:** Write a docstring even for `__init__`, but keep it short and to the point, e.g. `"""Constructs the ``Trainer`` object."""`.
110+
* **Public API coverage:** Always write docstrings for everything considered public API.
111+
87112
## Documentation
88113
89114
Documentation is built with **Zensical**.

d9d/core/autograd/grad_context.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44

55
class GradDirection(StrEnum):
6-
"""
7-
Enum representing the specific gradient edges to compute.
6+
"""Enum representing the specific gradient edges to compute.
87
98
This is used to manually control gradient flow in custom autograd functions
109
during split backward passes.
@@ -19,8 +18,7 @@ class GradDirection(StrEnum):
1918

2019

2120
class GlobalGradContext:
22-
"""
23-
Global state manager for controlling gradient computation in custom autograd functions.
21+
"""Global state manager for controlling gradient computation in custom autograd functions.
2422
2523
This context addresses a limitation in PyTorch where custom `torch.autograd.Function`
2624
implementations set `ctx.needs_input_grad` to True for all edges requiring grad,
@@ -38,13 +36,11 @@ class GlobalGradContext:
3836

3937
def __init__(self):
4038
"""Constructs a GlobalGradContext object with all directions enabled by default."""
41-
4239
# both directions by default
4340
self._enabled_directions: set[GradDirection] = {GradDirection.inputs, GradDirection.weight}
4441

4542
def check_direction(self, direction: GradDirection | None) -> bool:
46-
"""
47-
Checks if the gradient calculation for the given direction is currently enabled.
43+
"""Checks if the gradient calculation for the given direction is currently enabled.
4844
4945
Args:
5046
direction: The direction to check (inputs or weights). If None,
@@ -53,16 +49,14 @@ def check_direction(self, direction: GradDirection | None) -> bool:
5349
Returns:
5450
True if the direction is enabled or None is passed, False otherwise.
5551
"""
56-
5752
if direction is None:
5853
return True
5954

6055
return direction in self._enabled_directions
6156

6257
@contextmanager
6358
def with_directions(self, *directions: GradDirection):
64-
"""
65-
Context manager that sets the enabled gradient directions.
59+
"""Context manager that sets the enabled gradient directions.
6660
6761
This overrides the current state for the duration of the context
6862
and restores the previous state afterwards.

d9d/core/dist_context/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
This package configures the distributed environment and device meshes.
3-
"""
1+
"""This package configures the distributed environment and device meshes."""
42

53
from .configured import DistributedContext
64
from .device_mesh_domains import BATCH_DOMAIN, DENSE_DOMAIN, EXPERT_DOMAIN, FLAT_DOMAIN, REGULAR_DOMAIN

d9d/core/dist_context/configured.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ def _build_mesh_domains(params: "DeviceMeshParameters") -> dict[str, DeviceMesh]
3232

3333

3434
class DistributedContext:
35-
"""
36-
Acts as the single source of truth for the distributed execution environment.
35+
"""Acts as the single source of truth for the distributed execution environment.
3736
3837
It acts as the central repository for the distributed configuration, managing the creation
3938
and synchronization of PyTorch DeviceMeshes for different domains (Regular domain, Expert Parallel domain, ...).
@@ -78,12 +77,10 @@ def __init__(self, params: "DeviceMeshParameters", log_level: int):
7877
@property
7978
def logger(self) -> logging.Logger:
8079
"""Returns the logger instance configured for distributed logging."""
81-
8280
return self._logger
8381

8482
def mesh_for(self, domain: str) -> DeviceMesh:
85-
"""
86-
Returns the device mesh view associated with a specific logical domain.
83+
"""Returns the device mesh view associated with a specific logical domain.
8784
8885
Available Domains and Dimensions:
8986
* `regular` (`REGULAR_DOMAIN`): The most granular mesh for fully decomposed parallelism.
@@ -106,38 +103,32 @@ def mesh_for(self, domain: str) -> DeviceMesh:
106103
Raises:
107104
ValueError: If the specified domain does not exist.
108105
"""
109-
110106
if domain not in self._meshes:
111107
raise ValueError(f"Domain {domain} does not exist")
112108
return self._meshes[domain]
113109

114110
@property
115111
def is_main_process(self) -> bool:
116112
"""Checks if the current process is the global rank 0."""
117-
118113
return self._global_rank == 0
119114

120115
@property
121116
def is_local_main_process(self) -> bool:
122117
"""Checks if the current process is the rank 0 on the specific node."""
123-
124118
return self._local_rank == 0
125119

126120
def wait_world(self):
127121
"""Blocks process execution until all ranks reach this point."""
128-
129122
if self._params.is_distributed:
130123
torch.distributed.barrier(device_ids=[torch.cuda.current_device()])
131124
torch.cuda.synchronize()
132125

133126
def set_timeout(self, timeout_seconds: float):
134-
"""
135-
Updates the NCCL/process group timeout for all underlying meshes.
127+
"""Updates the NCCL/process group timeout for all underlying meshes.
136128
137129
Args:
138130
timeout_seconds: New timeout duration in seconds.
139131
"""
140-
141132
if not self._params.is_distributed: # does nothing for local setups
142133
return
143134

@@ -154,8 +145,7 @@ def set_timeout(self, timeout_seconds: float):
154145

155146
@contextmanager
156147
def local_main_process_first(self):
157-
"""
158-
Context manager that executes the block on the local main process first.
148+
"""Context manager that executes the block on the local main process first.
159149
160150
Other local ranks wait at the entrance. The local main process waits at the
161151
exit to synchronize before continuing.
@@ -170,13 +160,11 @@ def local_main_process_first(self):
170160

171161
@contextmanager
172162
def main_process_first(self):
173-
"""
174-
Context manager that executes the block on the global main process first.
163+
"""Context manager that executes the block on the global main process first.
175164
176165
All other ranks wait at the entrance. The global main process waits at the
177166
exit to synchronize before continuing.
178167
"""
179-
180168
if not self.is_main_process:
181169
self.wait_world()
182170

@@ -188,35 +176,29 @@ def main_process_first(self):
188176
@property
189177
def current_device(self) -> torch.device:
190178
"""Returns the CUDA device associated with this rank."""
191-
192179
return self._current_device
193180

194181
@property
195182
def mesh_params(self) -> "DeviceMeshParameters":
196183
"""Returns the parameters used to initialize this context."""
197-
198184
return self._params
199185

200186
@property
201187
def master_addr(self) -> str:
202188
"""Returns the IP address or domain name of the master node."""
203-
204189
return self._master_addr
205190

206191
@property
207192
def node_rank(self) -> int:
208193
"""Returns the index of the node this process is running on."""
209-
210194
return self._node_rank
211195

212196
@property
213197
def local_rank(self) -> int:
214198
"""Returns the rank of the current process within its node."""
215-
216199
return self._local_rank
217200

218201
@property
219202
def num_nodes(self) -> int:
220203
"""Returns the total number of nodes in the cluster."""
221-
222204
return self._num_nodes

d9d/core/dist_context/device_mesh_domains.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99

1010
class DeviceMeshDomain(abc.ABC):
11-
"""
12-
Abstract base class for a Device Mesh provider.
11+
"""Abstract base class for a Device Mesh provider.
1312
1413
A Domain defines a specific strategy for organizing available GPUs into a
1514
multidimensional grid (Mesh) to support specific parallelism techniques.
@@ -19,21 +18,18 @@ class DeviceMeshDomain(abc.ABC):
1918
@abc.abstractmethod
2019
def name(self) -> str:
2120
"""Returns the unique identifier for this mesh domain."""
22-
2321
...
2422

2523
@abc.abstractmethod
2624
def build_mesh(self, params: "DeviceMeshParameters") -> DeviceMesh:
27-
"""
28-
Constructs the device mesh configuration.
25+
"""Constructs the device mesh configuration.
2926
3027
Args:
3128
params: Global configuration parameters for the distributed environment.
3229
3330
Returns:
3431
The initialized PyTorch DeviceMesh for this specific domain.
3532
"""
36-
3733
...
3834

3935

d9d/core/dist_context/log.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44

55
def build_dist_logger(qualifier: str, level: int) -> logging.Logger:
6-
"""
7-
Configures and returns a logger instance for d9d.
6+
"""Configures and returns a logger instance for d9d.
87
98
The logger is configured to write to stdout with a formatter that includes
109
the provided rank qualifier, allowing for easier debugging in distributed logs.
@@ -16,7 +15,6 @@ def build_dist_logger(qualifier: str, level: int) -> logging.Logger:
1615
Returns:
1716
A configured logging.Logger instance.
1817
"""
19-
2018
dist_logger = logging.getLogger("d9d")
2119
dist_logger.setLevel(level)
2220
dist_logger.handlers.clear()

d9d/core/dist_context/params.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88

99
class DeviceMeshParameters(BaseModel):
10-
"""
11-
Configuration parameters for initializing Distributed Device Meshes.
10+
"""Configuration parameters for initializing Distributed Device Meshes.
1211
1312
Attributes:
1413
pipeline_parallel: Degree of pipeline parallelism (PP).
@@ -37,19 +36,16 @@ class DeviceMeshParameters(BaseModel):
3736
@property
3837
def has_pipeline_parallel(self) -> bool:
3938
"""Checks if pipeline parallelism is enabled (degree > 1)."""
40-
4139
return self.pipeline_parallel > 1
4240

4341
@property
4442
def has_data_parallel_replicate(self) -> bool:
4543
"""Checks if data parallel replication is enabled (degree > 1)."""
46-
4744
return self.data_parallel_replicate > 1
4845

4946
@property
5047
def has_data_parallel_shard(self) -> bool:
5148
"""Checks if data parallel sharding is enabled (degree > 1)."""
52-
5349
return self.data_parallel_shard > 1
5450

5551
@property
@@ -72,7 +68,6 @@ def has_expert_parallel(self) -> bool:
7268
@property
7369
def is_distributed(self) -> bool:
7470
"""Checks if any form of parallelism is enabled."""
75-
7671
return (
7772
self.has_pipeline_parallel
7873
or self.has_data_parallel_replicate
@@ -85,7 +80,6 @@ def is_distributed(self) -> bool:
8580

8681
@model_validator(mode="after")
8782
def _check_ep_divisibility(self) -> Self:
88-
"""Validates that DP/CP/TP dimensions can support the requested EP/ETP degrees."""
8983
dp_cp_tp_degree = (
9084
self.data_parallel_shard
9185
* self.data_parallel_replicate
@@ -103,11 +97,9 @@ def _check_ep_divisibility(self) -> Self:
10397
return self
10498

10599
def build(self, log_level: int = logging.INFO) -> "DistributedContext":
106-
"""
107-
Initializes the DistributedContext using these parameters.
100+
"""Initializes the DistributedContext using these parameters.
108101
109102
Returns:
110103
A new DistributedContext instance containing the initialized device meshes.
111104
"""
112-
113105
return DistributedContext(self, log_level)

d9d/core/dist_ops/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
This module provides high-level wrappers around `torch.distributed` collective operations.
3-
"""
1+
"""This module provides high-level wrappers around `torch.distributed` collective operations."""
42

53
from .object import all_gather_object, gather_object
64
from .tensor import all_gather, all_gather_variadic_shape, gather, gather_variadic_shape

d9d/core/dist_ops/object.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66

77

88
def gather_object(obj: T, group: dist.ProcessGroup, group_dst: int) -> list[T] | None:
9-
"""
10-
Gathers picklable objects from the whole process group to a specific destination rank.
9+
"""Gathers picklable objects from the whole process group to a specific destination rank.
1110
1211
This acts as a wrapper around torch.distributed.gather_object that automatically
1312
initializes the output buffer list on the destination rank.
@@ -20,7 +19,6 @@ def gather_object(obj: T, group: dist.ProcessGroup, group_dst: int) -> list[T] |
2019
Returns:
2120
A list of objects from all ranks on the destination rank; None on other ranks.
2221
"""
23-
2422
if group.rank() == group_dst:
2523
# We initialize with None, but we cast to list[T] because we know
2624
# dist.gather_object will populate these slots with actual objects.
@@ -32,8 +30,7 @@ def gather_object(obj: T, group: dist.ProcessGroup, group_dst: int) -> list[T] |
3230

3331

3432
def all_gather_object(obj: T, group: dist.ProcessGroup) -> list[T]:
35-
"""
36-
Gathers picklable objects from the whole process group to all ranks.
33+
"""Gathers picklable objects from the whole process group to all ranks.
3734
3835
This acts as a wrapper around torch.distributed.all_gather_object that automatically
3936
initializes the output buffer list on all ranks.

0 commit comments

Comments
 (0)