-
Notifications
You must be signed in to change notification settings - Fork 15
Sliding Window Attention (AR video gen) + KV cache lifecycle management #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
536dbe3
5405428
a2286d5
34600e3
e4bf410
a493cc6
5204339
433b26a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| model: "cosmos3" | ||
| # Sequence-length hint for the scheduler. The conductor only asserts its | ||
| # presence; the real per-request capacity is the KV pool below. | ||
| max_seq_len: 8192 | ||
| kv_cache: | ||
| max_num_pages: 1024 | ||
| # Windowed-AR serving config: same nano deployment as cosmos3_nano.yaml plus | ||
| # the opt-in windowed video walk (enable_windowed_video adds the | ||
| # vae_decoder_ar node and its streaming decoder partition; requests opt in | ||
| # per call via window_mode). Windowed requests run the eager denoise path, so | ||
| # capture stays enabled only for the t2i tiers it already covers. | ||
| model_kwargs: | ||
| cuda_graph: true | ||
| graph_max_latent_area: 2000 | ||
| enable_windowed_video: true | ||
| node_groups: | ||
| - node_names: ["dit"] | ||
| ranks: [0] | ||
| - node_names: ["vae_encoder", "vae_decoder", "vae_decoder_ar", "audio_decoder"] | ||
| ranks: [0] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| model: "cosmos3" | ||
| # Sequence-length hint for the scheduler (see cosmos3_nano.yaml). | ||
| max_seq_len: 8192 | ||
| # Per-rank KV pool (see cosmos3_nano_sp2.yaml). | ||
| kv_cache: | ||
| max_num_pages: 1024 | ||
| # cosmos3_nano_sp2.yaml plus the opt-in windowed video walk: the DiT (and its | ||
| # windowed commit pass) runs Ulysses sequence-parallel across two ranks; the | ||
| # streaming window decoder is small and runs un-sharded on rank 0, consuming | ||
| # the window-latents stream the multi-rank producer dedups to rank 0. | ||
| model_kwargs: | ||
| enable_windowed_video: true | ||
| node_groups: | ||
| - node_names: ["dit"] | ||
| ranks: [0, 1] | ||
| sp_size: 2 | ||
| - node_names: ["vae_encoder", "vae_decoder", "vae_decoder_ar", "audio_decoder"] | ||
| ranks: [0] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| model: "cosmos3" | ||
| # Sequence-length hint for the scheduler (see cosmos3_nano.yaml). | ||
| max_seq_len: 8192 | ||
| # Per-rank KV pool (see cosmos3_nano_tp2.yaml). | ||
| kv_cache: | ||
| max_num_pages: 1024 | ||
| # cosmos3_nano_tp2.yaml plus the opt-in windowed video walk: the DiT (and its | ||
| # windowed commit pass) runs tensor-parallel across two ranks; the streaming | ||
| # window decoder is small and runs un-sharded on rank 0, consuming the | ||
| # window-latents stream the multi-rank producer dedups to rank 0. | ||
| model_kwargs: | ||
| enable_windowed_video: true | ||
| node_groups: | ||
| - node_names: ["dit"] | ||
| ranks: [0, 1] | ||
| tp_size: 2 | ||
| - node_names: ["vae_encoder", "vae_decoder", "vae_decoder_ar", "audio_decoder"] | ||
| ranks: [0] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -548,6 +548,10 @@ def advance_seq_len(self, n: int | None = None, pos_id_n: int | None = None) -> | |||||
| for rid in self.request_ids: | ||||||
| state = self._get_state(rid) | ||||||
| state.seq_len += n | ||||||
| if n: | ||||||
| # Committed content grew — signal caches of prefix-derived | ||||||
| # views (see KVRequestState.prefix_epoch). | ||||||
| state.prefix_epoch += 1 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mstar/mstar/engine/cache_manager.py Lines 679 to 680 in 15634ae
|
||||||
| state.position_id_start += (pos_id_n if pos_id_n is not None else n) | ||||||
|
|
||||||
| @torch.compiler.disable | ||||||
|
|
@@ -601,6 +605,8 @@ def advance_seq_lens(self, pos_id_ns: list[int] | int | None = None) -> None: | |||||
| n = seq_lens[i] | ||||||
| state = self._get_state(rid, label=label) | ||||||
| state.seq_len += n | ||||||
| if n: | ||||||
| state.prefix_epoch += 1 | ||||||
| if pos_id_ns is None: | ||||||
| state.position_id_start += n | ||||||
| elif isinstance(pos_id_ns, int): | ||||||
|
|
@@ -614,6 +620,8 @@ def advance_seq_lens(self, pos_id_ns: list[int] | int | None = None) -> None: | |||||
| n = ps.seq_lens[i] | ||||||
| state = self._get_state(rid, label=label) | ||||||
| state.seq_len += n | ||||||
| if n: | ||||||
| state.prefix_epoch += 1 | ||||||
| if pos_id_ns is None: | ||||||
| if ps.custom_pos_advance is not None: | ||||||
| state.position_id_start += ps.custom_pos_advance[i] | ||||||
|
|
@@ -628,6 +636,26 @@ def advance_seq_lens(self, pos_id_ns: list[int] | int | None = None) -> None: | |||||
| for ps in self._plan_states.values(): | ||||||
| ps.custom_pos_advance = None | ||||||
|
|
||||||
| @torch.compiler.disable | ||||||
| def protect_prefix( | ||||||
| self, request_id: str, num_tokens: int, label: str | None = None, | ||||||
| ) -> None: | ||||||
| """Mark the first ``num_tokens`` of the request's stream (active label | ||||||
| unless given) as never releasable. See | ||||||
| ``PagedAllocationManager.protect_prefix``.""" | ||||||
| label = label or self.active_labels.get(request_id, "main") | ||||||
| self.alloc_manager.protect_prefix(request_id, label, num_tokens) | ||||||
|
|
||||||
| @torch.compiler.disable | ||||||
| def release_oldest( | ||||||
| self, request_id: str, num_tokens: int, label: str | None = None, | ||||||
| ) -> int: | ||||||
| """Free the oldest unprotected tokens of a live request, whole pages | ||||||
| only; returns tokens actually freed. See | ||||||
| ``PagedAllocationManager.release_oldest``.""" | ||||||
| label = label or self.active_labels.get(request_id, "main") | ||||||
| return self.alloc_manager.release_oldest(request_id, label, num_tokens) | ||||||
|
|
||||||
| @torch.compiler.disable | ||||||
| def snapshot_all( | ||||||
| self, from_label: str, | ||||||
|
|
@@ -1460,14 +1488,22 @@ def _run_dense_gen( | |||||
| offset = 0 | ||||||
| for idx, prefix_len, gen_len, state in dg["segs"]: | ||||||
| prefix_cache = state.dense_prefix_kv | ||||||
| if prefix_cache is None: | ||||||
| prefix_cache = state.dense_prefix_kv = {} | ||||||
| cached = prefix_cache.get(layer_idx) | ||||||
| if prefix_cache is None or prefix_cache.get("epoch") != state.prefix_epoch: | ||||||
| # First gather, or the committed content mutated since the | ||||||
| # last one (windowed generation appends/evicts per window — | ||||||
| # prefix_epoch moves with every such mutation): drop the stale | ||||||
| # clones and re-gather from the live pages. Static-prefix | ||||||
| # requests keep the single-gather behavior (their epoch never | ||||||
| # moves after prefill). | ||||||
| prefix_cache = state.dense_prefix_kv = { | ||||||
| "epoch": state.prefix_epoch, "layers": {}, | ||||||
| } | ||||||
| cached = prefix_cache["layers"].get(layer_idx) | ||||||
| if cached is None: | ||||||
| sub = kv_layer[idx] # [n_pages, 2, page_size, num_kv_heads, head_dim] | ||||||
| k_pref = sub[:, 0].reshape(-1, num_kv_heads, head_dim)[:prefix_len].clone() | ||||||
| v_pref = sub[:, 1].reshape(-1, num_kv_heads, head_dim)[:prefix_len].clone() | ||||||
| prefix_cache[layer_idx] = (k_pref, v_pref) | ||||||
| prefix_cache["layers"][layer_idx] = (k_pref, v_pref) | ||||||
| else: | ||||||
| k_pref, v_pref = cached | ||||||
| k_parts.append(k_pref) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,10 +16,19 @@ | |||||||||||||
|
|
||||||||||||||
| @dataclass | ||||||||||||||
| class OffloadedState: | ||||||||||||||
| """Tracks a single (request, label) that has been offloaded to CPU.""" | ||||||||||||||
| """Tracks a single (request, label) that has been offloaded to CPU. | ||||||||||||||
|
|
||||||||||||||
| Carries the full stream bookkeeping (not just seq_len/position) so a | ||||||||||||||
| reload restores a windowed label's protection/release state exactly, | ||||||||||||||
| rather than relying on the live KVRequestState object surviving the | ||||||||||||||
| offload untouched. | ||||||||||||||
| """ | ||||||||||||||
| cpu_page_indices: list[int] | ||||||||||||||
| seq_len: int | ||||||||||||||
| position_id_start: int | ||||||||||||||
| protected_prefix_tokens: int = 0 | ||||||||||||||
| released_tokens: int = 0 | ||||||||||||||
| prefix_epoch: int = 0 | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class CPUPagePool: | ||||||||||||||
|
|
@@ -68,6 +77,9 @@ def offload_pages( | |||||||||||||
| gpu_page_indices: list[int], | ||||||||||||||
| seq_len: int, | ||||||||||||||
| position_id_start: int, | ||||||||||||||
| protected_prefix_tokens: int = 0, | ||||||||||||||
| released_tokens: int = 0, | ||||||||||||||
| prefix_epoch: int = 0, | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mstar/mstar/engine/cpu_page_pool.py Lines 90 to 95 in 15634ae
This predates the PR, but the signature change lands on it and windowed requests raise its odds: they hold large streams for a long lifetime, which makes them natural eviction victims. When the CPU pool cannot fit the request, Could we return a bool from here and have |
||||||||||||||
| ) -> None: | ||||||||||||||
| """Copy GPU pages → CPU pages (async on dedicated stream).""" | ||||||||||||||
| n_pages = len(gpu_page_indices) | ||||||||||||||
|
|
@@ -94,6 +106,9 @@ def offload_pages( | |||||||||||||
| cpu_page_indices=cpu_pages, | ||||||||||||||
| seq_len=seq_len, | ||||||||||||||
| position_id_start=position_id_start, | ||||||||||||||
| protected_prefix_tokens=protected_prefix_tokens, | ||||||||||||||
| released_tokens=released_tokens, | ||||||||||||||
| prefix_epoch=prefix_epoch, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| def reload_pages( | ||||||||||||||
|
|
@@ -102,10 +117,11 @@ def reload_pages( | |||||||||||||
| label: str, | ||||||||||||||
| gpu_kv_cache: torch.Tensor, | ||||||||||||||
| gpu_page_indices: list[int], | ||||||||||||||
| ) -> tuple[int, int]: | ||||||||||||||
| ) -> OffloadedState: | ||||||||||||||
| """Copy CPU pages → GPU pages (async), free CPU pages. | ||||||||||||||
|
|
||||||||||||||
| Returns (seq_len, position_id_start) that were saved during offload. | ||||||||||||||
| Returns the ``OffloadedState`` saved during offload (its | ||||||||||||||
| ``cpu_page_indices`` are freed and no longer meaningful). | ||||||||||||||
| """ | ||||||||||||||
| state = self.offloaded[request_id][label] | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -117,11 +133,10 @@ def reload_pages( | |||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| self.page_allocator.free(state.cpu_page_indices) | ||||||||||||||
| seq_len, pos_id = state.seq_len, state.position_id_start | ||||||||||||||
| del self.offloaded[request_id][label] | ||||||||||||||
| if not self.offloaded[request_id]: | ||||||||||||||
| del self.offloaded[request_id] | ||||||||||||||
| return seq_len, pos_id | ||||||||||||||
| return state | ||||||||||||||
|
|
||||||||||||||
| def sync(self) -> None: | ||||||||||||||
| """Wait for all pending GPU↔CPU copies to complete.""" | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stream_videodecides the response shape here but is not a declared field onVideoGenerationRequest, so the endpoint's streaming mode is invisible in the protocol and this branch andcreate_videoseach fish it out of untyped kwargs. stream onChatCompletionRequestis declared, and both its readers share the one field. Could we declarestream_video: bool = Falsethe same way and have the adapter setdefault it into kwargs?