.. _basics-design-philosophy:
=====================================
:octicon:`infinity` Design Philosophy
=====================================
One of the core goals of fairseq2 is to make it possible for researchers to
explore new ideas and implement novel features without having to fork fairseq2.
Instead of having a monolithic repository that can only be modified by
copy-pasting large chunks of code, in fairseq2, all major APIs follow the
interface/implementation convention along with the `dependency inversion principle`__.
This means, each API has an *interface* (i.e. an abstract :class:`~abc.ABC`
class) that defines the contract of that API, and one or more concrete
implementations of that interface. Different implementations can be integrated
with the rest of fairseq2 via its powerful `dependency injection framework`__.
.. __: https://en.wikipedia.org/wiki/Dependency_inversion_principle
.. __: https://en.wikipedia.org/wiki/Dependency_injection
Interface/Implementation Convention
===================================
.. currentmodule:: fairseq2.nn
The diagram below shows the :doc:`position encoder API `
as an example. The API is defined by the abstract :class:`PositionEncoder`
PyTorch module. :class:`SinusoidalPositionEncoder`, :class:`LearnedPositionEncoder`,
and :class:`RotaryEncoder` implement :class:`PositionEncoder` for their
respective algorithms. Technically, any of these position encoders can be used
wherever a :class:`PositionEncoder` is expected (see `Dependency Inversion`_
below).
.. image:: /_static/img/position_encoder.svg
:width: 580px
:align: center
:alt: Position Encoder Hierarchy
These diagrams demonstrate fairseq2's interface-first approach: each API starts with
a clear abstract interface, followed by multiple concrete implementations that can
be used interchangeably.
Dependency Inversion
====================
.. currentmodule:: fairseq2.nn.transformer
The dependency inversion principle is critical to have a clean, well-tested, and
extensible API. The example below shows the (abbreviated) ``__init__()`` method
of the :class:`StandardTransformerDecoderLayer`::
class StandardTransformerDecoderLayer(TransformerDecoderLayer):
def __init__(
self,
self_attn: MultiheadAttention,
self_attn_layer_norm: LayerNorm,
encoder_decoder_attn: MultiheadAttention,
encoder_decoder_attn_layer_norm: LayerNorm,
ffn: FeedForwardNetwork,
ffn_layer_norm: LayerNorm,
*,
...
) -> None:
...
Instead of constructing the multihead attention and feed-forward network layers
within its ``__init__()`` method, :class:`StandardTransformerDecoderLayer`
expects the caller to provide instances of :class:`MultiheadAttention` and
:class:`FeedForwardNetwork` interfaces. This loose-coupling between an instance
and its dependencies enables composing diverse object graphs, such as different
model architectures, with minimal redundancy (i.e. code duplication).
Dependency Injection
====================
.. currentmodule:: fairseq2.runtime.dependency
fairseq2 v0.5 introduces a dependency injection framework that
significantly simplifies the construction and management of complex object graphs.
The core components are the :class:`DependencyContainer` and
:class:`DependencyResolver` classes, which provide automatic dependency resolution,
singleton management, and collection handling.
Core Components
^^^^^^^^^^^^^^^
The dependency injection system is built around several key abstractions:
.. autoclass:: DependencyResolver
:members: resolve, resolve_optional, iter_keys
:noindex:
.. autoclass:: DependencyContainer
:members: register, register_type, register_instance
:noindex:
.. autoclass:: DependencyProvider
:noindex:
Basic Usage
^^^^^^^^^^^
The fairseq2 library uses the dependency injection system extensively for all
core components. The global container is initialized by :func:`fairseq2.init_fairseq2`
and can be accessed through :func:`get_dependency_resolver`::
import fairseq2
from fairseq2.runtime.dependency import get_dependency_resolver
from fairseq2.assets import AssetStore
from fairseq2.device import Device
from fairseq2.models import load_model
# Initialize the library - sets up the global container
fairseq2.init_fairseq2()
# Access the global resolver
resolver = get_dependency_resolver()
# Resolve library components
asset_store = resolver.resolve(AssetStore)
device = resolver.resolve(Device)
card = asset_store.retrieve_card("llama3_1_8b_instruct")
# These are all automatically configured through the DI system
print(f"Default device: {device}")
print(f"Retrieved card: {card}")
Recipe Execution Flow
^^^^^^^^^^^^^^^^^^^^^
The diagram below illustrates how fairseq2's dependency injection system orchestrates
recipe execution, from initial composition through to task execution:
.. mermaid::
flowchart TD
%% Styling
classDef containerBox fill:#e1f5fe,stroke:#0288d1,stroke-width:2px,color:#01579b
classDef registryBox fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
classDef executionBox fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#1b5e20
classDef componentBox fill:#fff3e0,stroke:#f57c00,stroke-width:1px,color:#e65100
%% Step 0: Composition Phase
subgraph S0["🔧 Composition Phase"]
direction TB
subgraph Inputs["Core Dependencies"]
direction TB
WI[World Info]
DV[Device]
ENV[Environment]
FS[File System]
TH[Thread Pool]
RNG[RNG Bag]
componentBox:::componentBox
end
subgraph Registry["Extension Registry"]
direction TB
AS[Asset Store]
MF[Model Families]
TF[Tokenizer Families]
EX[Extensions]
CH[Checkpoint Handlers]
registryBox:::registryBox
end
subgraph Recipe["Recipe Components"]
direction TB
RC[Recipe Config]
OD[Output Directory]
TR[Task Runner]
executionBox:::executionBox
end
Inputs --> LR[Library Registration
_register_library]
Registry --> LR
LR --> RR[Recipe Registration
_register_*_recipe]
Recipe --> RUR[Run Registration
_register_run]
RR --> RUR
end
%% Dependency Container
RUR --> DC[📦 Dependency Container
Auto-wiring & Resolution]
DC:::containerBox
%% Step 1: Execution Phase
DC --> S1
subgraph S1["⚡ Execution Phase (_run_recipe)"]
direction TB
CP[Cluster Preparer
🏗️ Environment Setup]
LC[Log Configurer
📋 Distributed Logging]
CD[Config Dumper
💾 Save Configuration]
TC[Torch Configurer
🔥 PyTorch Setup]
LH[Log Helper
📊 System Info Logging]
TR2[Task Runner
🚀 Execute Recipe Task]
CP --> LC --> CD --> TC --> LH --> TR2
executionBox:::executionBox
end
%% Resolution arrows
DC -.->|"resolve(ClusterPreparer)"| CP
DC -.->|"resolve(LogConfigurer)"| LC
DC -.->|"resolve(ConfigDumper)"| CD
DC -.->|"resolve(TorchConfigurer)"| TC
DC -.->|"resolve(LogHelper)"| LH
DC -.->|"resolve(TaskRunner)"| TR2
This flow demonstrates several key concepts:
**1. Composition Phase** - All components are registered with the container:
- **Core Dependencies**: Essential fairseq2 components (Device, WorldInfo, etc.)
- **Extension Registry**: Pluggable components registered by extensions
- **Recipe Components**: Task-specific configuration and runners
**2. Dependency Container** - Acts as the central orchestrator:
- **Auto-wiring**: Automatically resolves dependencies through type inspection
- **Singleton Management**: Ensures single instances of expensive resources
- **Collection Support**: Handles multiple implementations (e.g., checkpoint loaders)
**3. Execution Phase** - Components are resolved and executed in sequence:
- Each component is resolved on-demand from the container
- Dependencies are automatically injected based on constructor annotations
- The execution order is deterministic and well-defined
See Also
=========
* :doc:`/reference/api/index` - API reference
* :doc:`/news/whats_new_v0_5` - What's new in v0.5 including DI improvements