Repository navigation
[Lumen-RL] Enable Rollout Router Replay (R3) #2274
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 9 commits
ae912f8
0dd2de0
c94c0b2
fe8c0b9
ed12c55
6a8d086
ec8239d
e6a574b
62332e6
6586c2c
96fbcf2
888abcd
52cb3d2
51bcc30
57cd818
3372d12
10added
a1850df
b340b8f
49144bc
ab2bb23
ce4b61d
53f46b7
95a7e6b
1696e67
b542967
18430bb
c271f13
ecac39b
9606dcd
b02deb7
af142eb
0e63603
cddc323
329b855
9a12cc8
fc666e6
ad10b75
9d4242c
dbd0631
c9e710c
e12acf1
3cdd10b
daeaaf7
f869895
1105f69
b79d1c1
e61a333
a5ff585
6d1929c
078cfc5
3783481
014f72f
b258308
9ea1bd6
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 |
|---|---|---|
|
|
@@ -1728,6 +1728,11 @@ class Config: | |
| kv_cache_dtype: str = "bf16" | ||
| index_cache_dtype: str | None = None | ||
| enable_prefix_caching: bool = True | ||
| # Return per-request MoE routes from generate() as int16 | ||
| # [seq_len - 1, num_layers, top_k]. Scatter is CUDA-graph-safe (device | ||
| # write inside fused MoE). Requires DCP=PCP=PP=1, no DP-attention, and | ||
| # no KV transfer / offload / RapidServe P/D; prefix cache stays on. | ||
| enable_return_routed_experts: bool = False | ||
|
Copilot marked this conversation as resolved.
Resolved
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. [15] Not in
Author
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. Added docs. |
||
| enable_chunked_prefill: bool = True | ||
| enable_log_stats: bool = True | ||
| # Seconds between engine-status lines. Validated > 0 by EngineStats. | ||
|
|
@@ -2142,6 +2147,24 @@ def __post_init__(self): | |
|
|
||
| self.kv_transfer_config = ast.literal_eval(self.kv_transfer_config) | ||
|
|
||
| if self.enable_return_routed_experts: | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| check_return_routed_experts, | ||
| ) | ||
|
|
||
| check_return_routed_experts( | ||
|
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. The same check is duplicated in atom/model_engine/scheduler.py:679 and atom/model_engine/model_runner.py:4723, each passing slightly different inputs (direct attributes vs getattr, explicit RapidServe flag). These will drift; please funnel all three through one helper that resolves the effective config. Also note plugin mode (vLLM/SGLang) is not refused here: the flag is accepted, but the ATOM ModelRunner path that implements capture never runs, so it silently becomes a no-op. Mono is already refused in atom/mono/runtime/deployment.py:120.
Author
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. Done. All three sites now call one helper, check_return_routed_experts_config(config), which reads the effective topology (including the combined RapidServe / disagg_is_decode / runner-class check) in one place. The RapidServe runner only adds rapidserve_runner=True. The scheduler and runner checks remain because they see the config again in their own processes. Plugin mode is now refused, and since the plugin builders never carried the flag, the vLLM and SGLang config translations now forward the host's enable_return_routed_experts so Config sees it and refuses, instead of the host flag silently doing nothing. Tests cover every RapidServe signal and the plugin refusal. |
||
| self.decode_context_parallel_size, | ||
| self.prefill_context_parallel_size, | ||
| self.pipeline_parallel_size, | ||
| kv_transfer_config=self.kv_transfer_config, | ||
| enable_rapidserve=( | ||
| self.enable_rapidserve | ||
| or self.disagg_is_decode | ||
| or "RapidServeModelRunner" in self.runner_qualname | ||
| ), | ||
| enable_dp_attention=self.enable_dp_attention, | ||
| ) | ||
|
|
||
| if self.speculative_config is not None: | ||
| num_spec = self.speculative_config.num_speculative_tokens | ||
| is_dspark = getattr(self.speculative_config, "use_dspark", lambda: False)() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,7 @@ | |
| Pool, | ||
| PoolPlan, | ||
| SubPoolSpec, | ||
| page_pool, | ||
| plan_pools, | ||
| ) | ||
| from atom.model_ops.decode_input_ids import ( | ||
|
|
@@ -1446,6 +1447,18 @@ def _sub_pool_specs(self) -> list[SubPoolSpec]: | |
| specs = list(self.attn_metadata_builder.sub_pool_specs()) | ||
| if hasattr(self, "draft_kv_builder"): | ||
| specs += self.draft_kv_builder.sub_pool_specs() | ||
| geo = self._routed_experts_geometry() | ||
| if geo is not None: | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| capture_bytes_per_kv_block, | ||
| ) | ||
|
|
||
| num_layers, top_k = geo | ||
| specs.append( | ||
| page_pool( | ||
| capture_bytes_per_kv_block(self.block_size, num_layers, top_k) | ||
| ) | ||
|
Comment on lines
+1792
to
+1794
Author
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. The geometry concern is right, though DeepSeek-V4 can't hit it. DeepSeek-V4 / V4.1: R3 refuses these builders before any surcharge is added. Both inherit CommonAttentionBuilder, where supplies_token_slots = False, so _routed_experts_geometry() raises at startup. Where it does cause an issue: Kimi's MLA+GDN builders (kimi_linear, glm5_next_text). They supply token slots, use PAGE checkpoints, and don't override checkpoint_page_unit_bytes(). R3 refuses pipeline parallelism and RapidServe, which are the only cases where Kimi turns checkpoints off, so with R3 on, page_unit_bytes took the merged "kv" charge including the route rows. PageUnitGeometryMixin._page_unit_regions then failed the equality check. Qwen GDN was unaffected because its override already narrows the unit to its MHA regions. Fix: I kept the route rows in "kv". They grow with the block count, so charging them per block is what keeps the capture buffer inside --gpu-memory-utilization. What changed is where the checkpoint unit comes from: the new checkpoint_page_unit_bytes(builder) in page_unit_checkpoint.py sums only the target builder's own PAGE specs, and a backend override can still narrow that further. Sizing still uses the merged charge. Why not refuse R3 when state_transfer().copies is on: that would also rule out R3 on Qwen3-Next and Qwen3.5-MoE, which work. |
||
| ) | ||
| return specs | ||
|
|
||
| def _state_pool_names(self) -> list[str]: | ||
|
|
@@ -1594,6 +1607,16 @@ def get_num_blocks(self) -> dict[str, object]: | |
|
|
||
| torch.set_default_device("cpu") | ||
|
|
||
| geo = self._routed_experts_geometry() | ||
| if geo is not None: | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| capture_pad_row_bytes, | ||
| ) | ||
|
|
||
| available_for_kv = max( | ||
| 0, int(available_for_kv) - capture_pad_row_bytes(*geo) | ||
| ) | ||
|
|
||
| specs = self._sub_pool_specs() | ||
|
|
||
| # Sub-pool sizing is pure arithmetic over the byte budget — see | ||
|
|
@@ -1972,6 +1995,11 @@ def allocate_kv_cache(self, num_kvcache_blocks): | |
| num_blocks=num_kvcache_blocks, | ||
| ) | ||
|
|
||
| # Capture buffer is part of the PAGE budget (per-slot int32 routes). | ||
| # Allocate it before the expected-vs-actual check so the measurement | ||
| # includes those bytes rather than warning on a false mismatch. | ||
| self._maybe_init_routed_experts_capturer(num_kvcache_blocks) | ||
|
sudhu2k marked this conversation as resolved.
Resolved
Copilot marked this conversation as resolved.
Resolved
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. [A4] Capture only where the result is used. Every TP rank allocates the
Author
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. Capturing only on rank 0 wouldn't gain anything. Every TP rank builds its KV cache with rank 0's block count, so the memory saved on the other ranks couldn't become extra KV. The ranks also sync at every all-reduce and rank 0 still does the scatter, so the step wouldn't get shorter. The overhead numbers in the PR were measured at TP1 anyway. Writing directly from select_experts would need an AITER kernel change. int16 would work and halve the buffer, but it shifts the KV memory budget, so I'd rather do it as a separate follow-up. |
||
|
|
||
| # Cross-validate: compare estimated vs actual KV cache allocation. | ||
| # `actual_kv_bytes` includes BOTH the unified pool tensors (counted by | ||
| # `block_bytes × num_blocks`) AND the per-request cache tensors (state | ||
|
|
@@ -2012,6 +2040,122 @@ def allocate_kv_cache(self, num_kvcache_blocks): | |
| torch.distributed.barrier() | ||
| return True | ||
|
|
||
| def _routed_experts_geometry(self) -> tuple[int, int] | None: | ||
| """``(num_layers, top_k)`` for the capture buffer, or None if unused. | ||
|
|
||
| Assigns ``moe_capture_layer_id`` on each FusedMoE. Cached so pool | ||
| planning and capturer init share one walk. | ||
| """ | ||
| if hasattr(self, "_routed_experts_geo"): | ||
| return self._routed_experts_geo | ||
| if not getattr(self.config, "enable_return_routed_experts", False): | ||
| self._routed_experts_geo = None | ||
| return None | ||
| if not hasattr(self, "model"): | ||
| return None | ||
| import re | ||
|
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. function-local import.. move re to module level
Author
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. Fixed. |
||
|
|
||
| from atom.model_ops.fused_moe.routed_experts_capturer import fused_moe_modules | ||
|
|
||
| moes: list = fused_moe_modules(self.model) | ||
| if not moes: | ||
| raise ValueError( | ||
| "enable_return_routed_experts requires at least one FusedMoE " | ||
| "module; this model has none (dense checkpoints cannot " | ||
|
Author
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. The current code already handles this comment. |
||
| "return routed_experts)" | ||
| ) | ||
| layer_ids: list[int] = [] | ||
| for ordinal, moe in enumerate(moes): | ||
| prefix = getattr(moe, "prefix", "") or "" | ||
| match = re.search(r"layers\.(\d+)", prefix) | ||
|
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. [A9] Reuse the existing layer-index helper.
Author
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. Done for the layer index. _routed_experts_geometry now uses extract_layer_index(moe.prefix), with no regex and no layer_id or ordinal fallback. |
||
| if match: | ||
| idx = int(match.group(1)) | ||
| elif getattr(moe, "layer_id", None) is not None: | ||
| idx = int(moe.layer_id) | ||
| else: | ||
| idx = ordinal | ||
| moe.moe_capture_layer_id = idx | ||
| layer_ids.append(idx) | ||
| self._routed_experts_geo = ( | ||
|
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. The layer axis is max(MoE layer index) + 1, not the model's layer count. For checkpoints with dense layers (mlp_only_layers / decoder_sparse_step > 1, both supported in atom/models/qwen3_moe.py:309), trailing dense layers make this dimension shorter than num_hidden_layers, so the exported shape no longer matches the documented [seq_len-1, num_layers, top_k] contract and downstream consumers will misindex layers. Dense layers in the middle read back as -1. Two MoE modules resolving to the same id would also silently overwrite each other. Please derive the axis from the model config, and assert that the resolved ids are unique.
Author
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. Agreed. The layer axis is now |
||
| max(layer_ids) + 1, | ||
| max(int(m.top_k) for m in moes), | ||
| ) | ||
|
Comment on lines
+2531
to
+2534
Author
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. Fixed this. |
||
| return self._routed_experts_geo | ||
|
|
||
| def _maybe_init_routed_experts_capturer(self, num_kvcache_blocks: int) -> None: | ||
| geo = self._routed_experts_geometry() | ||
| if geo is None: | ||
| return | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| RoutedExpertsCapturer, | ||
| ) | ||
|
|
||
| num_layers, top_k = geo | ||
| num_slots = int(num_kvcache_blocks) * int(self.block_size) | ||
| RoutedExpertsCapturer.init( | ||
| num_slots=num_slots, | ||
| num_layers=num_layers, | ||
| top_k=top_k, | ||
| device=self.device, | ||
| ) | ||
|
|
||
| def _store_routed_experts_step( | ||
| self, | ||
| batch: ScheduledBatch, | ||
| *, | ||
| stream: torch.cuda.Stream | None = None, | ||
| wait_event: torch.cuda.Event | None = None, | ||
| slot_mapping: torch.Tensor | None = None, | ||
| ) -> bool: | ||
| if not getattr(self.config, "enable_return_routed_experts", False): | ||
| return False | ||
| if getattr(batch, "is_dummy_run", False) or self.rank != 0: | ||
| return False | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| RoutedExpertsCapturer, | ||
| ) | ||
|
|
||
| capturer = RoutedExpertsCapturer.get() | ||
| if capturer is None: | ||
| return False | ||
| return capturer.store_step(slot_mapping, stream=stream, wait_event=wait_event) | ||
|
|
||
| def _commit_routed_experts(self, *, keep_last: bool = False) -> None: | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| RoutedExpertsCapturer, | ||
| ) | ||
|
|
||
| capturer = RoutedExpertsCapturer.get() | ||
| if capturer is not None: | ||
| capturer.commit_pending(keep_last=keep_last) | ||
|
|
||
| def _attach_routed_experts( | ||
| self, batch: ScheduledBatch, output: ScheduledBatchOutput | ||
| ) -> None: | ||
| """CPU gather in this runner process. The capturer is not visible to | ||
| the EngineCore scheduler (runner is an ``AsyncIOProcManager`` child). | ||
| """ | ||
| if not getattr(self.config, "enable_return_routed_experts", False): | ||
| return | ||
| if getattr(batch, "is_dummy_run", False) or self.rank != 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. [5] (also A4) Only rank 0 reads, but every rank allocates the buffer and scatters every MoE layer. Keep the PAGE charge on all ranks so block counts match, and create the capturer on rank 0 only; the hook is already a no-op without one.
Author
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. Done. The capturer is now created on TP rank 0 only, at startup and on wake, so the other ranks' MoE hook does nothing and they skip the buffer and the per-layer scatter. Every rank still charges the capture rows to its KV pool, so block counts match across ranks. store_step's separate rank check is gone, since "no capturer" already covers it. Added test_only_tp_rank0_allocates_the_capture_buffer. |
||
| return | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| RoutedExpertsCapturer, | ||
| ) | ||
|
|
||
| capturer = RoutedExpertsCapturer.get() | ||
| if capturer is None: | ||
| return | ||
| block_tables = getattr(batch, "block_tables", None) | ||
| if not block_tables or len(block_tables) != len(batch.req_ids): | ||
| return | ||
| output.routed_experts = capturer.export_batch( | ||
| batch.req_ids, | ||
| block_tables, | ||
| batch.context_lens, | ||
| self.block_size, | ||
| ) | ||
|
|
||
| def get_dp_padding(self, num_tokens: int) -> tuple[int, torch.Tensor | None]: | ||
| dp_size = self.config.parallel_config.data_parallel_size | ||
| dp_rank = self.config.parallel_config.data_parallel_rank | ||
|
|
@@ -3080,11 +3224,21 @@ def postprocess( | |
| sampled_logprobs = get_tp_group().broadcast(sampled_logprobs, src=0) | ||
|
|
||
| self.forward_done_event.record() | ||
| # Capture before prepare_sampled_ids(), which advances self.prev_batch to current batch. | ||
| deferred = self.tokenID_processor.is_deferred_out | ||
| # Capture before prepare_sampled_ids(), which advances self.prev_batch. | ||
| prev_batch = self.tokenID_processor.prev_batch | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| _current_slot_mapping, | ||
| ) | ||
|
|
||
| slots = _current_slot_mapping() | ||
|
|
||
| token_id_dict, logprobs_map = self.tokenID_processor.prepare_sampled_ids( | ||
| batch, sampled_tokens, self.forward_done_event, sampled_logprobs | ||
| ) | ||
| # Blocking D2H after recv: previous copy_done already waited, so this | ||
| # memcpy does not absorb the current decode. cpu_buffer is complete | ||
| # before the next step attaches this batch as prev_batch. | ||
| self._store_routed_experts_step(batch, slot_mapping=slots) | ||
| # Extract req_ids and token_ids from dict (key -1 is the is_deferred_out flag) | ||
| req_ids_out = [k for k in token_id_dict if k != -1] | ||
| token_ids_out = [token_id_dict[k] for k in req_ids_out] | ||
|
|
@@ -3151,7 +3305,7 @@ def postprocess( | |
| if verify_scheduler is not None: | ||
| dspark_ell = verify_scheduler.ell_nonblocking() | ||
|
|
||
| return ScheduledBatchOutput( | ||
| output = ScheduledBatchOutput( | ||
| req_ids=req_ids_out, | ||
| token_ids=token_ids_out, | ||
| draft_token_ids=draft_token_ids, | ||
|
|
@@ -3161,6 +3315,10 @@ def postprocess( | |
| logprobs=logprobs_map, | ||
| dspark_ell=dspark_ell, | ||
| ) | ||
| attach_batch = prev_batch if deferred and prev_batch is not None else batch | ||
| if not (deferred and prev_batch is None): | ||
| self._attach_routed_experts(attach_batch, output) | ||
| return output | ||
|
|
||
| def _record_kv_cache_ready(self, batch: ScheduledBatch) -> None: | ||
| """Publish a GPU event for final prefill chunks to transfer connectors.""" | ||
|
|
@@ -3250,17 +3408,21 @@ def forward(self, batch: ScheduledBatch) -> ScheduledBatchOutput: | |
| None, # nothing verified -> segment's last row | ||
| align_only=True, | ||
| ) | ||
| self.forward_done_event.record() | ||
|
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. [A3] Don't block on the chunked-prefill middle-chunk path. Here
Author
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. Thanks, both are valid. A2: Removed. That record() was left over from an earlier version and nothing on this path reads it. A3: Fixed. Middle chunks now store their routes on the async copy stream, so the CPU no longer waits for the chunk's forward. The routes are handed to the scheduler with the next output, merged in step order with that output's own routes. The scheduler already handles a one-step delay like this on the deferred path. A small correction on the impact: GPU work wasn't serialized, but every middle chunk did lose the CPU/GPU overlap, so the GPU sat idle while the next chunk was being scheduled. I added tests for the ordering in both deferred and non-deferred modes. |
||
| # No token copy_done on this path; blocking store is fine. | ||
| self._store_routed_experts_step(batch) | ||
|
sudhu2k marked this conversation as resolved.
Resolved
|
||
| reset_forward_context() | ||
| # Mark this slot's GPU work (attention consumed its metadata) done. | ||
| self._record_forward_vars_event() | ||
| self._record_kv_cache_ready(batch) | ||
| return ScheduledBatchOutput( | ||
| output = ScheduledBatchOutput( | ||
| req_ids=list(batch.req_ids), | ||
| token_ids=[], | ||
| num_rejected=None, | ||
| num_bonus=None, | ||
| draft_token_ids=None, | ||
| ) | ||
| return output | ||
|
|
||
| fwd_output = self.postprocess( | ||
| batch, | ||
|
|
@@ -4213,9 +4375,29 @@ def _kv_budget_extra_reserve(self, total_bytes: int) -> int: | |
| safety_margin = int(total_bytes * 0.02) | ||
| return 4 * safety_margin | ||
|
|
||
| def _refuse_routed_experts_capture(self) -> None: | ||
| if not getattr(self.config, "enable_return_routed_experts", False): | ||
| return | ||
| from atom.model_ops.fused_moe.routed_experts_capturer import ( | ||
| check_return_routed_experts, | ||
| ) | ||
|
|
||
| check_return_routed_experts( | ||
| getattr(self.config, "decode_context_parallel_size", 1), | ||
| getattr(self.config, "prefill_context_parallel_size", 1), | ||
| getattr(self.config, "pipeline_parallel_size", 1), | ||
| kv_transfer_config=getattr(self.config, "kv_transfer_config", None), | ||
| enable_rapidserve=True, | ||
| enable_dp_attention=bool( | ||
| getattr(self.config, "enable_dp_attention", False) | ||
| ), | ||
| ) | ||
|
|
||
| def get_num_blocks(self) -> dict[str, object]: | ||
| # Decode in disagg mode owns no GPU memory — kvcache is imported from | ||
| # prefill. | ||
| # prefill. That also skips `_maybe_init_routed_experts_capturer`, so | ||
| # capture cannot be served from this process. | ||
| self._refuse_routed_experts_capture() | ||
| if self.config.disagg_is_decode: | ||
| transfer = self.attn_metadata_builder.state_transfer() | ||
| if transfer.copies: | ||
|
|
@@ -4231,6 +4413,7 @@ def get_num_blocks(self) -> dict[str, object]: | |
|
|
||
| def allocate_kv_cache(self, num_kvcache_blocks): | ||
| # Decode in disagg mode: kvcache is imported from prefill, not allocated. | ||
| self._refuse_routed_experts_capture() | ||
| if self.config.disagg_is_decode: | ||
| logger.info("decode skipping kv cache allocation") | ||
| return True | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,16 +2,17 @@ | |
| # Copyright (C) 2024-2025, Advanced Micro Devices, Inc. All rights reserved. | ||
|
|
||
| from dataclasses import dataclass | ||
| from typing import Any, Dict, List, Optional | ||
| from typing import Any | ||
|
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. [A2] Please revert unrelated formatting changes:
Author
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. Reverted the extra local in the aborted |
||
|
|
||
|
|
||
| @dataclass | ||
| class RequestOutput: | ||
| """Output structure passed to stream callback.""" | ||
|
|
||
| request_id: int | ||
| output_tokens: List[int] | ||
| output_tokens: list[int] | ||
| finished: bool | ||
| finish_reason: Optional[str] = None | ||
| kv_transfer_params_output: Optional[Dict[str, Any]] = None | ||
| finish_reason: str | None = None | ||
| kv_transfer_params_output: dict[str, Any] | None = None | ||
| num_cached_tokens: int = 0 | ||
| routed_experts: Any | None = None | ||
|
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. Any hides the contract that downstream code depends on: np.ndarray of shape [seq_len-1, num_layers, top_k], dtype int16, only present on the final output. It is also only surfaced through the generate() dict (atom/model_engine/llm_engine.py:972) and not through the OpenAI HTTP response, which is fine but worth stating explicitly. Please tighten the type to np.ndarray | None and document both the shape and the "final output only" rule.
Author
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. Done: routed_experts is now np.ndarray | None, and the field documents the contract. It's read-only int16 [seq_len - 1, num_hidden_layers, top_k] with -1 on dense layers. It's set only on the final output (None on streamed ones) and only exposed through generate(), not the OpenAI HTTP response. |
||
Uh oh!
There was an error while loading. Please reload this page.