Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
e993809
nemotron_duplex: capture the streaming encoder's layer stack
dghosef Oct 19, 2026
7f919cb
nemotron_duplex: capture the codec's steady-state decode
dghosef Oct 18, 2026
e3fba43
nemotron_duplex codec: build the iSTFT window envelope once per shape
dghosef Oct 18, 2026
ad124b5
nemotron_duplex encoder: bounded front end in a pool, a live step as …
dghosef Oct 18, 2026
a612053
R1.1: the codec's inverse STFT keeps no envelope per length
dghosef Oct 19, 2026
876bdcb
R1.2: refuse a session cap a per-session resource cannot hold
dghosef Oct 19, 2026
0732042
R1.16: drive the encoder's and codec's regions through the real runner
dghosef Oct 19, 2026
1cd9bd6
R1.4: the encoder reads each pool's plan where it uses it, before a r…
dghosef Oct 19, 2026
49d6cc7
R1.5: store the encoder's layer state in the dtype the layers compute…
dghosef Oct 19, 2026
01a07ef
R1.6: the layer region runs only the calls that got a frame
dghosef Oct 19, 2026
493448f
R1.10: test the recurrent pool's gather / scatter_, and refuse the se…
dghosef Oct 19, 2026
e6e0882
R1.11: gather reads with the plan's int32 indices, without a cast
dghosef Oct 19, 2026
12ddec7
R1.7: describe the encoder's pools and graphs as they are
dghosef Oct 19, 2026
a115c76
R1.13: tell live clients which append size takes the encoder's fast path
dghosef Oct 19, 2026
76f6b7f
R1.15: say that per-message deflate is off for every WebSocket route
dghosef Oct 19, 2026
5ee201a
R1.17: one list of session batch sizes; the codec reads its runners d…
dghosef Oct 19, 2026
739b56d
R2.1: check the codec's window envelope on the host, so it builds on …
dghosef Oct 19, 2026
44c8105
R2.2: cap the encoder's and codec's batches at their widest graph
dghosef Oct 19, 2026
4cfac81
R2.3: size and check nano_kv for the session cap, as talker_kv is
dghosef Oct 19, 2026
1d042b6
R2.4: the encoder's layer state takes the engine's compute dtype
dghosef Oct 19, 2026
0164d83
R2.5: per-message deflate is a serving setting, off in the duplex con…
dghosef Oct 19, 2026
98ecd5d
R2.6: the streamer takes any featurizer geometry the offline encoder …
dghosef Oct 19, 2026
fc5db60
R2.7: refuse a checkpoint the declared resources were not sized for
dghosef Oct 19, 2026
a322b95
R2.8: tell live clients the whole-step graph keeps to the call's fram…
dghosef Oct 19, 2026
ba262f7
R2.9: decide once whether a call takes the whole-step region
dghosef Oct 19, 2026
1d09a92
R2.10: an encoder step copies its calls' chunks to the device at once
dghosef Oct 19, 2026
9306c94
R2.13: the encoder tests take their region leases from the engine's o…
dghosef Oct 19, 2026
bca60c8
R2.14: replay the encoder's step region and the codec's region with a…
dghosef Oct 19, 2026
6737e6d
R2.16: the default session count is its own constant, not the capture…
dghosef Oct 19, 2026
6304de4
R2.17: correct the duplex docs and comments the review found stale
dghosef Oct 19, 2026
26d6221
R1.9: keep cuFFT out of the captured encoder step and codec decode
dghosef Oct 19, 2026
1873ecb
nemotron_duplex: the encoder no longer takes max_session_samples
dghosef Oct 21, 2026
7497253
nemotron_duplex: separate long encoder chunks by can_batch; drop Recu…
dghosef Oct 21, 2026
aaa25e2
nemotron_duplex tests: capture on CPU as main's runner does; the engi…
dghosef Oct 21, 2026
e4030ee
test_recurrent_padding_rows: drop the stale select wording from a doc…
dghosef Oct 21, 2026
246222c
Merge branch 'duplex/nemotron-live-v3' into duplex/perf-v3
dghosef Oct 21, 2026
791c31f
Bounded KV: a sink + window per-request KV resource
NSagan271 Oct 21, 2026
e2cd2e1
Bounded KV: a per-query causal window
dghosef Oct 21, 2026
951c94a
Bounded KV: guard the online softmax against a fully masked tile
dghosef Oct 21, 2026
7c9d2cb
Bounded KV: cast the probabilities to the values' dtype before the dot
dghosef Oct 21, 2026
a51fc80
Bounded KV: dtype=None is the engine's KV dtype
dghosef Oct 21, 2026
69c7a7a
Bounded KV: rename the graph-buffer hook to main's name
dghosef Oct 21, 2026
8cdc9e8
Bounded KV: admit refusals name the request at fault
dghosef Oct 21, 2026
32fb8fc
Bounded KV: commit skips span-0 segments
dghosef Oct 21, 2026
8cff7bb
Bounded KV: drop force_double_buffer, whose hazard this resource lacks
dghosef Oct 21, 2026
d67309c
Bounded KV: tests for the per-query window, bf16, span 0 and the hooks
dghosef Oct 21, 2026
94e5067
docs: bounded KV's per-query window and dtype=None
dghosef Oct 21, 2026
de6be73
Bounded KV: the per-query window is a config field, retention may be …
dghosef Oct 21, 2026
6cc08d0
Bounded KV: resolve retention once per source length
dghosef Oct 21, 2026
e03a4a2
Bounded KV: per step, a row attends only the step's keys it writes
dghosef Oct 21, 2026
2acf397
Bounded KV: pick the query tile from T and the element size
dghosef Oct 21, 2026
c0e4499
Bounded KV: rollback_admit gives back the slots an admit took
dghosef Oct 21, 2026
710b188
Bounded KV: refuse arguments the kernel would misread
dghosef Oct 21, 2026
0abfcf4
Bounded KV: say the online softmax's -inf guard is defensive; undangl…
dghosef Oct 22, 2026
bb0fb52
Bounded KV: only CPU tensors take the torch path
dghosef Oct 22, 2026
ff59d32
Bounded KV: resolve dtype=None without writing it into the config
dghosef Oct 22, 2026
521566a
nemotron_duplex: the encoder's attention window on bounded KV
dghosef Oct 21, 2026
e716ec2
nemotron_duplex: a steady call falls back to the eager path again
dghosef Oct 22, 2026
b5124e0
nemotron_duplex: the front end makes the frames prepare_inputs counted
dghosef Oct 22, 2026
68cd0d8
nemotron_duplex: a call's KV position is built once per step
dghosef Oct 22, 2026
0b7b7de
nemotron_duplex: name the all-steady declaration test for its three r…
dghosef Oct 22, 2026
0e7fb5c
nemotron_duplex: the module docstring lists the encoder's bounded KV
dghosef Oct 22, 2026
c00670c
Merge branch 'duplex/nemotron-live' into duplex/perf
dghosef Oct 22, 2026
d475815
Merge remote-tracking branch 'origin/duplex/nemotron-live' into HEAD
dghosef Oct 26, 2026
9e45084
Merge duplex/nemotron-live (with origin/main) into duplex/perf
dghosef Oct 27, 2026
f4ac338
Merge follow-up: the encoder plan test checks only the KV's plan unde…
dghosef Oct 27, 2026
c361af0
Merge merge/594-main (round-1 fixes R1.1-R1.3) into merge/595-main
dghosef Oct 27, 2026
9a9ac66
Merge merge/594-main (P3b) into merge/595-main
dghosef Oct 27, 2026
27925f8
P3b: nemotron_duplex configs (#595 wording): a live call past the cap…
dghosef Oct 27, 2026
f0ef4a5
P3a: the recurrent pool resolves a dtype=None block without writing i…
dghosef Oct 27, 2026
51a07e0
P3a: size the recurrent pool's startup log from its resolved blocks
dghosef Oct 27, 2026
f3d3c1a
R2.3: linear-attn managers take the state dtype the pool stores
dghosef Oct 27, 2026
e1d22cc
R2.4: delete RecurrentBlockConfig's unused byte sizes
dghosef Oct 27, 2026
b08a02c
Merge merge/594-main (R2.1, R2.2) into merge/595-main
dghosef Oct 27, 2026
c2110d6
Merge merge/594-main into merge/595-main (R3.1)
dghosef Oct 27, 2026
afa4364
R3.2: resolve a recurrent block's dtype on RecurrentBlockConfig
dghosef Oct 27, 2026
0587065
R3.4: two blank lines before RecurrentStateSpec; fold a test import
dghosef Oct 27, 2026
838b5d8
Merge merge/594-main into merge/595-main (test fakes for #593 fields)
dghosef Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions configs/nemotron_duplex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@
model: "nemotron_duplex"
max_seq_len: 12288 # config: voicechat streaming_max_len / vllm_max_model_len

# One recurrent slot per session (DEFAULT_MAMBA_SLOTS); past it the engine spins
# on admit and wedges every session. Keep the two equal; a live call past the
# cap gets a 503, and only one-shot requests queue.
# Sessions at once; a live call past it gets a 503 (only one-shot requests
# queue). Each session holds a slot in
# resources mamba_state, encoder_state, encoder_front and encoder_kv and its
# pages in nano_kv and talker_kv, all sized for 32 (DEFAULT_SESSIONS); to raise
# the cap, raise their max_slots (cap + 1 for the recurrent pools, for the
# sink; cap for encoder_kv) and the KV pools' max_num_pages with it.
# The server refuses to start with a cap one of them cannot hold.
max_concurrent_requests: 32

# No per-message deflate on the WebSocket routes: compressing 32 live calls'
# 80 ms appends cost the event loop ~8% on /generate/ws (H100, 2026-10-17).
ws_per_message_deflate: false

# A live call's input files over its whole life; past it the call ends with a
# 400. 160 s of 16 kHz audio is about 10 MB as float32 WAV: 32 leaves room for
# stereo or a 48 kHz opening clip. A file is decoded at most one frame past
Expand Down
14 changes: 11 additions & 3 deletions configs/nemotron_duplex_disagg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@
model: "nemotron_duplex"
max_seq_len: 12288

# One recurrent slot per session (DEFAULT_MAMBA_SLOTS); past it the engine spins
# on admit and wedges every session. Keep the two equal; a live call past the
# cap gets a 503, and only one-shot requests queue.
# Sessions at once; a live call past it gets a 503 (only one-shot requests
# queue). Each session holds a slot in
# resources mamba_state, encoder_state, encoder_front and encoder_kv and its
# pages in nano_kv and talker_kv, all sized for 32 (DEFAULT_SESSIONS); to raise
# the cap, raise their max_slots (cap + 1 for the recurrent pools, for the
# sink; cap for encoder_kv) and the KV pools' max_num_pages with it.
# The server refuses to start with a cap one of them cannot hold.
max_concurrent_requests: 32

# No per-message deflate on the WebSocket routes: compressing 32 live calls'
# 80 ms appends cost the event loop ~8% on /generate/ws (H100, 2026-10-17).
ws_per_message_deflate: false

# A live call's input files over its whole life; past it the call ends with a
# 400. 160 s of 16 kHz audio is about 10 MB as float32 WAV: 32 leaves room for
# stereo or a 48 kHz opening clip. A file is decoded at most one frame past
Expand Down
13 changes: 9 additions & 4 deletions docs/adding_models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,12 @@ The spec types are:
``SamplingReqConfig`` above.
* - ``RecurrentStateSpec(config=RecurrentStateConfig(...))``
- Fixed-size per-request state that carries between steps (an LSTM's ``h`` and
``c``, a cursor, a conv tail): one slot per request, ``blocks`` naming a shape and
dtype for each piece, ``num_layers`` copies of each, ``max_slots`` slots with slot 0
the sink that padding rows address. ``declare_step`` gives each row a
``c``, a cursor, a conv tail, a Mamba or gated-DeltaNet layer's conv window and SSM
state, a streaming encoder's caches): one slot per request and label, ``blocks``
naming a shape and dtype for each piece (dtype None: the engine's compute dtype,
what its KV caches store), ``num_layers`` copies of each, ``max_slots`` slots
(deployment-tunable) with slot 0 the sink that padding rows address. A recurrent
kernel reads the step's ``addressing``. ``declare_step`` gives each row a
``RecurrentStep`` segment with span >= 1. The forward reads its rows with
``pool.gather(name, start=...)`` and writes them back with
``pool.scatter_(name, value)``; both follow the step's plan, so there is no
Expand Down Expand Up @@ -866,7 +869,9 @@ within itself and matches the offline mask. ``rel_bias`` is then indexed by that
``head_dim ** -0.5``). A per-query window takes no sink, and its right context is 0.
``retention`` may be a single ``SinkWindow`` when it does not depend on the source length.
``BoundedKVConfig(dtype=None)`` stores the cache in the engine's KV dtype, for layers that
compute in it.
compute in it. Nemotron VoiceChat's streaming encoder (``ENCODER_KV``,
``encoder_kv_config`` in ``mstar/model/nemotron_duplex/components/conformer.py``) is the
per-query example.

Step 3 — Declare the computation graph
--------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions docs/clients.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ WebSocket ``/generate/ws``

``/generate/ws`` is the same request over one persistent WebSocket, for control loops
that cannot afford an HTTP round trip per step (robot policies, streaming world models).
The server negotiates per-message deflate on its WebSocket routes unless the deployment
turns it off (``ws_per_message_deflate`` in :doc:`serving`, as the Nemotron VoiceChat
configs do).
Each message is one request with the ``/generate`` fields — ``text``, ``files`` as
``[{"name": ..., "data": ...}]``, ``input_modalities``, ``output_modalities``,
``model_kwargs``, ``request_id`` — sent either as a JSON text frame (``data`` base64) or
Expand Down Expand Up @@ -189,6 +192,12 @@ is at ``max_concurrent_requests`` ends at once with ``status`` 503, rather than
slot and start that far behind its input; a one-shot request waits in the queue. Other
models ignore ``end_input``, and an append to them is rejected.
Live input needs the Python frontend: ``--rust-frontend`` serves no ``/generate/ws``.
The append size that serves best is the model's: Nemotron VoiceChat runs a live step as one
CUDA graph only for an append of exactly 80 ms (1280 samples at 16 kHz) onto a call whose
audio so far is a whole number of 80 ms frames. Keep the opening clip and every append a
multiple of 1280 samples: one that is not (a one-second opening clip, 16000 samples) keeps
the call off the graph for the rest of its life. Other sizes are served the same, more
slowly (see :doc:`models`).

.. code-block:: python

Expand Down
42 changes: 37 additions & 5 deletions docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -597,18 +597,31 @@ Nemotron VoiceChat (``nemotron_duplex``) notes
PCM: each 5-frame chunk is decoded with the previous 3 frames as left context,
which covers the decoder's receptive field, and a window's last 12 samples,
the inverse-STFT overlap, are held back and emitted with the next chunk, so
the stream equals the full-history decode sample for sample).
the stream equals the full-history decode sample for sample; that steady
window of 8 frames replays a CUDA graph, other windows decode eagerly).
The decode loops are stream-terminated: they end when the upstream stream
closes, not on EOS, which is an ordinary per-frame token in duplex speech.
- ``nano_llm`` declares a paged KV cache over its four attention layers with
the attention plan on it (``resources: nano_kv`` / ``nano_attn``; no
positional encoding, so no position resource), a recurrent-state pool holding
the 27 Mamba-2 layers' conv window and SSM state, one slot per session
(``mamba_state``: 32 sessions by default, ~137 MB per slot in fp32, tunable
with ``max_slots`` / ``state_dtype``; the shipped configs set
``max_concurrent_requests: 32`` to match, so raise the two together), the Mamba-2 resource planned on it
with ``max_slots``), the Mamba-2 resource planned on it
(``mamba``) and the agent-text sampler (``nano_sampler``). Its decode step is
captured as a CUDA graph for batch sizes 1 to 32.
- Every per-session resource is sized for 32 sessions: a slot in each of
``mamba_state``, ``encoder_state`` and ``encoder_front`` (``max_slots``, one
more than the sessions, for the sink) and in ``encoder_kv`` (``max_slots``,
no sink), and its pages in ``nano_kv`` and
``talker_kv`` (``max_num_pages``; both sized from ``max_session_s``, and the
nano's from ``max_prompt_tokens`` too). The shipped configs set
``max_concurrent_requests: 32`` to match; to serve more, raise the cap and
all six with it. The server
refuses to start with a cap one of them cannot hold, since a session past
any of them would wait on admit, and its frames with it, until another ends.
The nano, the talker, the encoder and the codec each run at most 32 sessions
a step, their widest CUDA graph; more are split over several steps rather
than run eagerly.
- ``eartts_talker`` advances every live session in one backbone pass per frame
through the engine's paged KV (``talker_kv`` / ``talker_attn`` /
``talker_pos``): each session owns two streams, the conditional and the
Expand All @@ -634,8 +647,12 @@ Nemotron VoiceChat (``nemotron_duplex``) notes
fast as it can). Live calls paced in real time over ``/generate/ws``
(``test/nemotron_duplex/live_ws_request.py``, 80 ms appends, 2026-10-17, one
H100): input-to-token lag p50 48 ms with one call and about 100-120 ms with
8, which keep up; from 24 calls up the server falls behind real time, so lag
grows over the call. Served audio is checked for intelligibility by
8, which keep up; from 24 calls up the server fell behind real time, so lag
grew over the call. Capturing the encoder's live step (below) took its host
time at 32 calls from 23.6 to 8.4 ms and cut live lag 2-6x at 16-32 calls
(paced runs interleaved against the uncaptured encoder, ``MSTAR_SHM_ARENA=1``);
32 calls staggered over 2 s all finish, heard latency p50 2.1-2.3 s (from the
user stopping to agent audio playable without gaps). Served audio is checked for intelligibility by
transcribing it (``test/nemotron_duplex/asr_check.py``, Whisper
large-v3-turbo): MaskGIT sampling is knife-edge, so attention-backend
numerics change the waveform but not the words.
Expand Down Expand Up @@ -682,6 +699,21 @@ Nemotron VoiceChat (``nemotron_duplex``) notes
(inferred from those frames, not measured on served text); under the served
bf16 a different chunking can flip a knife-edge frame such as the agent's
turn start.
- The encoder keeps each call's state in a slot per session of three
resources: its attention keys and values in a bounded KV (``encoder_kv``),
which keeps a call's last 70 frames per layer and runs the attention, each new
frame over the 70 before it, with the relative-position term as a band of
distances; its conv inputs (``encoder_state``); and its newest audio and mel
frames (``encoder_front``), the last two recurrent-state pools. A
call that appends one frame of audio (1280 samples) on the frame grid, every
live step of an 80 ms client, runs its whole step as a CUDA graph (the grid is
the call's start: an opening clip or append that is not a multiple of 1280
samples keeps the call off it for good); other calls take the front end
eagerly, then the layer stack as a graph when each got at most one frame (a
live step, the call's end), or eagerly (a one-shot upload, a large append:
a chunk over a second is not batched with other calls, so the engine runs
each call of that step on its own). Appends of other sizes are served the same,
without the whole-step graph.
- Checks: ``test/nemotron_duplex/duplex_request.py`` prepares a clip from the
bundled ``turn_taking.wav`` and sends it; ``parity_compare.py`` runs the
standalone reference path on the same clip and checks the served text
Expand Down
8 changes: 8 additions & 0 deletions docs/serving.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ A config maps the model's computation-graph nodes to physical GPU ranks. The key
``max_call_input_mb`` it counts what the model was handed, not bytes uploaded.
* - ``model_kwargs``
- *(optional)* Server-init model parameters (see below).
* - ``ws_per_message_deflate``
- *(optional, default true)* Whether the server offers per-message deflate on its
WebSocket routes (``/generate/ws``, ``/v1/realtime``); it applies to every route.
Deflate shrinks 80 ms speech frames to 49-73% of their size as base64 JSON and to
55-90% as binary PCM, and digital silence to 1-2%, but compressing every frame
costs the event loop: about 8% of it at 32 live Nemotron VoiceChat calls on
``/generate/ws``, whose configs turn it off. Turn it off where the server's
event loop, not the network, is the bottleneck.
* - ``speculation_mixes_walks``
- *(optional)* Per node, ``true``/``false`` for every walk of the node's combined
walks, or a mapping of walk -> ``true``/``false``. On for a walk, a speculation
Expand Down
14 changes: 11 additions & 3 deletions mstar/api_server/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,16 @@ async def shutdown_event():
# CLI entry point
# ------------------------------------------------------------------

def _uvicorn_config(app, host: str, port: int, config: dict) -> uvicorn.Config:
# Per-message deflate is negotiated server-wide, for every WebSocket route,
# so it is the deployment's to turn off (see ws_per_message_deflate in
# docs/serving.rst); on by default, as uvicorn's.
return uvicorn.Config(
app, host=host, port=port, access_log=False,
ws_per_message_deflate=bool(config.get("ws_per_message_deflate", True)),
)


def _set_preprocess_threads(model, config: dict) -> None:
"""Size torch's intra-op pool for this process's media preprocessing.

Expand Down Expand Up @@ -2593,9 +2603,7 @@ def main(argv: list[str] | None = None):
logger.info("Starting mstar API server on %s:%s", args.host, args.port)
# uvicorn.run() inlined so the server object is reachable. The
# message thread stops it when the conductor dies.
server = uvicorn.Server(
uvicorn.Config(app, host=args.host, port=args.port, access_log=False)
)
server = uvicorn.Server(_uvicorn_config(app, args.host, args.port, config))

def _stop_server():
server.should_exit = True
Expand Down
4 changes: 2 additions & 2 deletions mstar/engine/resources/linear_attn/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def build(cls, spec: LinearAttnSpec, info: EngineResourceInfo):
config=spec.config,
geometry=Mamba2Geometry.from_blocks(pool_config.blocks),
num_layers=pool_config.num_layers,
state_dtype=pool_config.blocks["ssm"].dtype,
state_dtype=pool_config.blocks["ssm"].resolved_dtype(info.kv_dtype),
has_sink=not pool_config.disable_sink_slot,
device=info.device,
)
Expand All @@ -55,7 +55,7 @@ def build(cls, spec: LinearAttnSpec, info: EngineResourceInfo):
max_rows_in_combined=info.max_rows_in_combined,
geometry=geometry,
num_layers=pool_config.num_layers,
state_dtype=pool_config.blocks["state"].dtype,
state_dtype=pool_config.blocks["state"].resolved_dtype(info.kv_dtype),
has_sink=not pool_config.disable_sink_slot,
device=info.device,
)
Expand Down
31 changes: 10 additions & 21 deletions mstar/engine/resources/recurrent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from abc import ABC, abstractmethod
from dataclasses import dataclass, field, replace
from math import prod
from typing import TYPE_CHECKING

import torch
Expand All @@ -24,17 +23,23 @@ class RecurrentBlockConfig:
"""One per-slot, per-layer tensor block.

``shape`` is opaque to the pool. ``shard_dims`` names the axes divided
across ranks.
across ranks. A ``dtype`` of None is the engine's compute dtype (what its
KV caches store), resolved by ``resolved_dtype`` and never written back.
"""

shape: tuple[int, ...]
dtype: torch.dtype
dtype: torch.dtype | None
shard_dims: tuple[int, ...] = ()

def __post_init__(self):
self.shape = tuple(self.shape)
self._unsharded_shape = self.shape

def resolved_dtype(self, kv_dtype: torch.dtype | None) -> torch.dtype:
"""The dtype the pool stores this block in; a resource planned against
the pool reads it here rather than from ``dtype``."""
return self.dtype or kv_dtype or torch.get_default_dtype()

def shard(self, num_shards: int) -> None:
from mstar.distributed.utils import divide

Expand All @@ -43,14 +48,6 @@ def shard(self, num_shards: int) -> None:
shape[dim] = divide(self._unsharded_shape[dim], num_shards)
self.shape = tuple(shape)

@property
def numel(self) -> int:
return prod(self.shape)

@property
def nbytes(self) -> int:
return self.numel * torch.empty((), dtype=self.dtype).element_size()


class RecurrentGeometry(ABC):
@abstractmethod
Expand Down Expand Up @@ -91,7 +88,7 @@ def conv_dim(self) -> int:

def to_blocks(
self,
state_dtype: torch.dtype = torch.float32,
state_dtype: torch.dtype | None = torch.float32,
conv_dtype: torch.dtype = torch.bfloat16,
) -> dict[str, RecurrentBlockConfig]:
"""Pool blocks for this geometry; head counts are pre-sharding."""
Expand Down Expand Up @@ -183,7 +180,7 @@ def conv_dim(self) -> int:

def to_blocks(
self,
state_dtype: torch.dtype = torch.float32,
state_dtype: torch.dtype | None = torch.float32,
conv_dtype: torch.dtype = torch.bfloat16,
) -> dict[str, RecurrentBlockConfig]:
"""Pool blocks for this geometry; head counts pre-sharding (heads and
Expand Down Expand Up @@ -276,14 +273,6 @@ def shard(self, num_shards: int) -> None:
for block in self.blocks.values():
block.shard(num_shards)

@property
def slot_bytes(self) -> int:
return self.num_layers * sum(b.nbytes for b in self.blocks.values())

@property
def total_bytes(self) -> int:
return self.slot_bytes * self.max_slots


@dataclass
class RecurrentStateSpec(NodeResourceSpec):
Expand Down
17 changes: 12 additions & 5 deletions mstar/engine/resources/recurrent/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ def build(
config = spec.config
if info.joint_comm_group is not None:
config.shard(info.joint_comm_group.world_size)
return cls(device=info.device, config=config, name=spec.resource_key)

def __init__(self, device: torch.device, config: RecurrentStateConfig, name: str = "recurrent_state"):
# resolved here, not written back: the config is the model's, shared by its specs
return cls(device=info.device, config=config, name=spec.resource_key, kv_dtype=info.kv_dtype)

def __init__(
self, device: torch.device, config: RecurrentStateConfig, name: str = "recurrent_state",
kv_dtype: torch.dtype | None = None,
):
"""``kv_dtype`` (the engine's) stands in for a block's ``dtype=None``."""
self._device = device
self.config = config
# the resource key, for errors
Expand All @@ -90,7 +95,7 @@ def __init__(self, device: torch.device, config: RecurrentStateConfig, name: str
self._blocks: dict[str, torch.Tensor] = {
name: torch.zeros(
(config.num_layers, config.max_slots, *block.shape),
dtype=block.dtype,
dtype=block.resolved_dtype(kv_dtype),
device=device,
)
for name, block in config.blocks.items()
Expand Down Expand Up @@ -127,12 +132,14 @@ def __init__(self, device: torch.device, config: RecurrentStateConfig, name: str
self._preplanned = False
self._cached_plan_output: dict[str, RecurrentAddressing] | None = None

# from the blocks, not the config: a dtype=None block is sized as resolved
total_bytes = sum(t.nbytes for t in self._blocks.values())
logger.info(
"recurrent state pool: %d usable slots (+%d sink) x %d layers, "
"%.2f MiB (%.2f KiB/slot), blocks=%s",
config.usable_slots, config.max_slots - config.usable_slots,
config.num_layers,
config.total_bytes / 2**20, config.slot_bytes / 2**10,
total_bytes / 2**20, total_bytes / config.max_slots / 2**10,
{n: tuple(b.shape) for n, b in config.blocks.items()},
)

Expand Down
Loading
Loading