Skip to content

Refactor serving code - #113

Merged
superxf merged 2 commits into
hw-native-sys:mainfrom
superxf:refactor_serving
Jul 28, 2026
Merged

Refactor serving code#113
superxf merged 2 commits into
hw-native-sys:mainfrom
superxf:refactor_serving

Conversation

@superxf

@superxf superxf commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Refactor serving code

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87ea70cc-3381-4e10-86c1-7abe857daba4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR reorganizes serving around canonical configuration and core types, model plugins, dedicated KV-cache components, replica engines, worker IPC services, shared HTTP response helpers, and architecture-boundary validation. Compatibility re-exports preserve legacy import paths while new tests cover routing, caching, lifecycle, registry, and worker behavior.

Changes

Serving architecture refactor

Layer / File(s) Summary
Canonical contracts and core types
pypto_serving/config/*, pypto_serving/core/*, pypto_serving/config/types.py
Configuration, request, batch, model, and tokenizer types move into focused modules with compatibility re-exports.
Model integration contracts
pypto_serving/model/plugin.py, pypto_serving/model/registry.py, pypto_serving/model/deepseek/*, pypto_serving/model/qwen/*
Model plugins, registries, DeepSeek V4 contracts, and extracted Qwen runtime/input components are added.
Cache and scheduler ownership
pypto_serving/serving/memory/*, pypto_serving/serving/sched/*
KV-cache allocation, host storage, grouped pools, and scheduler cache operations are separated behind dedicated components and protocols.
Serving runtime and worker flow
pypto_serving/serving/engine/*, pypto_serving/serving/worker/*, pypto_serving/serving/server/*, pypto_serving/serving/composition.py
Replica execution, load-aware routing, output processing, HTTP response helpers, worker IPC, and runtime composition are introduced or refactored.
Architecture validation
tests/lint/check_architecture.py, tests/test_*.py, .pre-commit-config.yaml, docs/architecture/*
Dependency-boundary linting, architecture tests, lifecycle/cache/registry/worker tests, documentation, and a pre-commit hook validate the refactor.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit hops through layers wide,
Plugins guide the model’s stride.
Caches bloom and workers call,
Tokens stream through one-way hall.
Old paths stay, while new paths gleam—
Architecture joins the dream.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the changes, but it is too generic to convey the main refactor. Use a more specific title that names the main serving refactor, such as model/plugin registry or serving engine composition.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is related to the changeset, but it is very brief.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
tests/test_architecture.py (1)

28-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ignore comments and docstrings when detecting model branches.

This flags any incidental qwen/deepseek mention, including documentation, as an architecture violation. Inspect executable AST branch expressions instead, so the test enforces its stated contract without blocking harmless documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_architecture.py` around lines 28 - 36, Update the
architecture-violation scan in the generic_roots loop to parse each Python
file’s AST and inspect only executable branch expressions for “qwen” and
“deepseek”, excluding comments and docstrings. Preserve the existing violation
reporting format and relative-path behavior while avoiding raw source-text
matching.
pypto_serving/serving/composition.py (1)

60-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Migrate the engine lifecycle to a lifespan handler.

@app.on_event("startup")/@app.on_event("shutdown") is deprecated in FastAPI; use an asynccontextmanager with FastAPI(lifespan=lifespan) instead for the engine start/stop and merge profile shutdown flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/composition.py` around lines 60 - 67, Replace the
startup and shutdown event handlers with an asynccontextmanager lifespan handler
that starts the engine before yielding, then stops the engine and invokes
shutdown_callback afterward. Pass this handler via FastAPI(lifespan=lifespan),
preserving the existing engine lifecycle and profile shutdown behavior.
pypto_serving/serving/worker/service.py (1)

363-375: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the duplicated supports_device_embedding branches.

prev_token_tensor is assigned once in the else branch (host-embedding, line 371) and again via a separate re-check of the same condition (lines 374-375) for the device-embedding path. Both branches are mutually exclusive today, but splitting the assignment across two independent if checks on the same condition is confusing and easy to break under future edits.

♻️ Proposed consolidation
             decode_token_tensor = torch.tensor(decode_tokens, dtype=torch.long, device=device)
             if self.executor.supports_device_embedding:
                 # Device kernel embeds directly from token ids — do not build
                 # host-side embedding tensors.
                 decode_embeddings = None
                 prev_embeddings = None
+                prev_token_tensor = torch.tensor(prev_tokens, dtype=torch.long, device=device)
             else:
                 decode_embeddings = self.executor.lookup_embeddings(runtime_model, decode_token_tensor)
                 prev_token_tensor = torch.tensor(prev_tokens, dtype=torch.long, device=device)
                 prev_embeddings = self.executor.lookup_embeddings(runtime_model, prev_token_tensor)
-
-            if self.executor.supports_device_embedding:
-                prev_token_tensor = torch.tensor(prev_tokens, dtype=torch.long, device=device)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/worker/service.py` around lines 363 - 375, Consolidate
the duplicate supports_device_embedding checks in the decode-token setup so
prev_token_tensor is assigned within one mutually exclusive branch structure.
Preserve the existing behavior: create prev_token_tensor for device embedding
without host embeddings, and create it before prev_embeddings in the
host-embedding path.
pypto_serving/serving/server/responses.py (2)

95-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

__all__ is not sorted per Ruff (RUF022).

♻️ Proposed fix
 __all__ = [
-    "FinalGeneration",
-    "SSE_DONE",
-    "collect_final_generation",
-    "cumulative_text_delta",
-    "map_finish_reason",
-    "sse_model",
-    "to_generate_config",
-    "usage_from_output",
+    "collect_final_generation",
+    "cumulative_text_delta",
+    "FinalGeneration",
+    "map_finish_reason",
+    "SSE_DONE",
+    "sse_model",
+    "to_generate_config",
+    "usage_from_output",
 ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/server/responses.py` around lines 95 - 104, Sort the
exported names in the module-level __all__ list alphabetically to satisfy Ruff
rule RUF022, preserving all existing exports.

Source: Linters/SAST tools


88-89: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use model_dump_json() for SSE payload serialization.

sse_model is on the streaming hot path, and model.model_dump_json() uses Pydantic’s direct JSON serializer, avoiding the unnecessary model_dump()json.dumps() round trip. The output whitespace may differ from the current json.dumps() call, but it is still compact and matches Pydantic v2’s idiomatic API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/server/responses.py` around lines 88 - 89, Update
sse_model to serialize the BaseModel directly with model_dump_json() instead of
model_dump() followed by json.dumps(), while preserving the existing SSE “data:”
prefix and double-newline terminator.
pypto_serving/model/registry.py (1)

46-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unreachable ambiguity branch in detect.

The loop returns on the first matching non-fallback plugin, so control only reaches Line 57 when no plugin matched (and no fallback is registered). At that point matches is always empty, making the len(matches) == 1 and "matches multiple registered families" branches dead code — the intended multi-match ambiguity error can never fire. Consider simplifying to make the actual behavior explicit:

♻️ Proposed simplification (behavior-preserving)
     def detect(self, config_data: dict[str, object]) -> ModelPlugin:
         """Detect a model family, using the registered fallback last."""
         fallback = None
         for plugin in self._plugins:
             if plugin.is_fallback:
                 fallback = plugin
                 continue
             if plugin.matches(config_data):
                 return plugin
         if fallback is not None:
             return fallback
-        matches = [plugin for plugin in self._plugins if plugin.matches(config_data)]
-        if len(matches) == 1:
-            return matches[0]
-        if not matches:
-            raise ValueError("Could not detect a registered model family")
-        families = ", ".join(plugin.family for plugin in matches)
-        raise ValueError(f"Model metadata matches multiple registered families: {families}")
+        raise ValueError("Could not detect a registered model family")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/model/registry.py` around lines 46 - 63, Update
ModelPluginRegistry.detect to remove the unreachable matches-count and ambiguity
handling after the plugin loop. Preserve first-match behavior for non-fallback
plugins, return the registered fallback when present, and otherwise raise the
existing “Could not detect a registered model family” error.
pypto_serving/serving/memory/kv_cache.py (2)

34-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

__all__ not sorted (RUF022).

Ruff flags this list as not case-insensitively sorted — NONE_HASH should come after hash_block_tokens. Since coding guidelines require following the ruff.toml configuration, this will fail lint if RUF022 is enabled in CI.

🔧 Proposed fix
 __all__ = [
     "FreeKVCacheBlockQueue",
     "KVCacheBlock",
     "KVCacheBlocks",
     "KVCacheCapacityError",
     "KvCacheManager",
-    "NONE_HASH",
     "hash_block_tokens",
+    "NONE_HASH",
 ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/memory/kv_cache.py` around lines 34 - 42, Sort the
exported names in __all__ case-insensitively to satisfy RUF022, placing
hash_block_tokens before NONE_HASH while preserving all existing exports.

Source: Linters/SAST tools


299-307: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Manager reaches into private members of GroupedKVCacheAllocator.

_candidate_group_partitions, _select_group_partition, and _group_pool call self._group_allocator._candidate_partitions, ._select_partition, and ._pool — all underscore-prefixed (private) methods of another class. This crosses the encapsulation boundary the refactor is otherwise establishing between KvCacheManager and the allocator.

Consider promoting these to public methods on GroupedKVCacheAllocator (or dropping the leading underscore) so KvCacheManager depends only on its public surface.

Also applies to: 341-346

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/memory/kv_cache.py` around lines 299 - 307, Promote the
allocator operations used by KvCacheManager to the public
GroupedKVCacheAllocator API: expose candidate partition selection, partition
selection, and pool access without underscore-prefixed names. Update
_candidate_group_partitions, _select_group_partition, and _group_pool to call
the new public symbols, and update all corresponding definitions and call sites
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pypto_serving/serving/engine/async_engine.py`:
- Around line 95-97: Update AsyncEngine.stop to call asyncio.gather with
return_exceptions=True, ensuring every reversed self._cores replica completes
its stop coroutine even when one fails. Preserve propagation or handling of
shutdown errors according to the existing stop contract after all cores have
finished.

In `@pypto_serving/serving/engine/output.py`:
- Around line 110-141: Remove completed request contexts from
ReplicaEngineCore._request_contexts: in pypto_serving/serving/engine/output.py
lines 110-141, delete the entry after request_output.finished handling; in
pypto_serving/serving/engine/replica.py lines 509-520, change the request lookup
to pop(request_id, None) so worker-error completion also cleans up safely.

In `@pypto_serving/serving/server/responses.py`:
- Around line 42-49: Handle the backend failure reason emitted by
replica.py::_handle_step_error before it is normalized by map_finish_reason:
update collect_final_generation and the streaming handlers in server.py to
detect output.finish_reason == "error" and propagate a distinct client-visible
failure, using an HTTP 5xx for non-streaming responses and an SSE error event or
equivalent logged streaming error. Keep map_finish_reason’s existing default
behavior for genuinely unmapped reasons.

In `@pypto_serving/serving/worker/service.py`:
- Around line 151-157: Update the exception handler in busy_loop so it logs the
caught queue-read exception with the existing logger before breaking out of the
loop, matching the diagnostic behavior of the other exception handlers in the
file.

In `@tests/test_kv_cache.py`:
- Around line 55-67: Update test_grouped_cache_growth_is_atomic_across_groups to
use asymmetric capacities so ensure_group_blocks("second", ...) allocates from
an earlier group before failing in a later group. Assert the later failure
leaves group_request_partition("second") unset and the earlier group’s
allocation returned, then preserve the existing release and successful retry
checks with expectations adjusted to the asymmetric pool sizes.

---

Nitpick comments:
In `@pypto_serving/model/registry.py`:
- Around line 46-63: Update ModelPluginRegistry.detect to remove the unreachable
matches-count and ambiguity handling after the plugin loop. Preserve first-match
behavior for non-fallback plugins, return the registered fallback when present,
and otherwise raise the existing “Could not detect a registered model family”
error.

In `@pypto_serving/serving/composition.py`:
- Around line 60-67: Replace the startup and shutdown event handlers with an
asynccontextmanager lifespan handler that starts the engine before yielding,
then stops the engine and invokes shutdown_callback afterward. Pass this handler
via FastAPI(lifespan=lifespan), preserving the existing engine lifecycle and
profile shutdown behavior.

In `@pypto_serving/serving/memory/kv_cache.py`:
- Around line 34-42: Sort the exported names in __all__ case-insensitively to
satisfy RUF022, placing hash_block_tokens before NONE_HASH while preserving all
existing exports.
- Around line 299-307: Promote the allocator operations used by KvCacheManager
to the public GroupedKVCacheAllocator API: expose candidate partition selection,
partition selection, and pool access without underscore-prefixed names. Update
_candidate_group_partitions, _select_group_partition, and _group_pool to call
the new public symbols, and update all corresponding definitions and call sites
consistently.

In `@pypto_serving/serving/server/responses.py`:
- Around line 95-104: Sort the exported names in the module-level __all__ list
alphabetically to satisfy Ruff rule RUF022, preserving all existing exports.
- Around line 88-89: Update sse_model to serialize the BaseModel directly with
model_dump_json() instead of model_dump() followed by json.dumps(), while
preserving the existing SSE “data:” prefix and double-newline terminator.

In `@pypto_serving/serving/worker/service.py`:
- Around line 363-375: Consolidate the duplicate supports_device_embedding
checks in the decode-token setup so prev_token_tensor is assigned within one
mutually exclusive branch structure. Preserve the existing behavior: create
prev_token_tensor for device embedding without host embeddings, and create it
before prev_embeddings in the host-embedding path.

In `@tests/test_architecture.py`:
- Around line 28-36: Update the architecture-violation scan in the generic_roots
loop to parse each Python file’s AST and inspect only executable branch
expressions for “qwen” and “deepseek”, excluding comments and docstrings.
Preserve the existing violation reporting format and relative-path behavior
while avoiding raw source-text matching.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87c160ba-4264-4a31-b8b3-bf0b9bc1ff68

📥 Commits

Reviewing files that changed from the base of the PR and between c217603 and ecf8abf.

📒 Files selected for processing (73)
  • .pre-commit-config.yaml
  • docs/architecture/overview.md
  • docs/architecture/refactoring.md
  • examples/model/qwen3_14b/npu_generate.py
  • pypto_serving/__init__.py
  • pypto_serving/cli/main.py
  • pypto_serving/config/__init__.py
  • pypto_serving/config/generation.py
  • pypto_serving/config/runtime.py
  • pypto_serving/config/serving.py
  • pypto_serving/config/types.py
  • pypto_serving/core/__init__.py
  • pypto_serving/core/batch.py
  • pypto_serving/core/model.py
  • pypto_serving/core/request.py
  • pypto_serving/core/tokenizer.py
  • pypto_serving/model/common/executor/executor.py
  • pypto_serving/model/common/executor/pypto_executor.py
  • pypto_serving/model/common/executor/sampler.py
  • pypto_serving/model/common/runner/model_runner.py
  • pypto_serving/model/deepseek/cache.py
  • pypto_serving/model/deepseek/contracts.py
  • pypto_serving/model/deepseek/input_builder.py
  • pypto_serving/model/deepseek/npu_executor.py
  • pypto_serving/model/deepseek/npu_runner.py
  • pypto_serving/model/deepseek/plugin.py
  • pypto_serving/model/deepseek/runtime_types.py
  • pypto_serving/model/model_loader.py
  • pypto_serving/model/plugin.py
  • pypto_serving/model/qwen/cache_policy.py
  • pypto_serving/model/qwen/input_builder.py
  • pypto_serving/model/qwen/l3_runtime.py
  • pypto_serving/model/qwen/npu_executor.py
  • pypto_serving/model/qwen/npu_runner.py
  • pypto_serving/model/qwen/plugin.py
  • pypto_serving/model/qwen/runtime_types.py
  • pypto_serving/model/registry.py
  • pypto_serving/model/tokenizer.py
  • pypto_serving/serving/composition.py
  • pypto_serving/serving/engine/async_engine.py
  • pypto_serving/serving/engine/engine.py
  • pypto_serving/serving/engine/output.py
  • pypto_serving/serving/engine/replica.py
  • pypto_serving/serving/engine/request_state.py
  • pypto_serving/serving/engine/routing.py
  • pypto_serving/serving/memory/block_pool.py
  • pypto_serving/serving/memory/grouped_cache.py
  • pypto_serving/serving/memory/host_cache.py
  • pypto_serving/serving/memory/kv_cache.py
  • pypto_serving/serving/sched/cache.py
  • pypto_serving/serving/sched/cache_coordinator.py
  • pypto_serving/serving/sched/scheduler.py
  • pypto_serving/serving/sched/types.py
  • pypto_serving/serving/server/ipc.py
  • pypto_serving/serving/server/ports.py
  • pypto_serving/serving/server/responses.py
  • pypto_serving/serving/server/schemas.py
  • pypto_serving/serving/server/server.py
  • pypto_serving/serving/server/serving_worker.py
  • pypto_serving/serving/worker/__init__.py
  • pypto_serving/serving/worker/client.py
  • pypto_serving/serving/worker/protocol.py
  • pypto_serving/serving/worker/service.py
  • tests/lint/check_architecture.py
  • tests/test_architecture.py
  • tests/test_batching.py
  • tests/test_composition.py
  • tests/test_kv_cache.py
  • tests/test_model_registry.py
  • tests/test_package.py
  • tests/test_scheduler_policy.py
  • tests/test_serving_worker.py
  • tests/test_worker_client.py

Comment on lines 95 to 97
async def stop(self) -> None:
"""Stop all DP engine cores."""
"""Stop all data-parallel replica cores."""
await asyncio.gather(*(core.stop() for core in reversed(self._cores)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

stop() can abandon other replicas' shutdown if one core fails.

Without return_exceptions=True, if one core's stop() raises, the exception propagates immediately while the other cores' stop() coroutines (already scheduled by gather) are left running unsupervised. If the caller (e.g. the FastAPI shutdown handler) doesn't keep the event loop alive long enough, other replicas' worker subprocesses may never be cleanly closed.

🛡️ Proposed fix
     async def stop(self) -> None:
         """Stop all data-parallel replica cores."""
-        await asyncio.gather(*(core.stop() for core in reversed(self._cores)))
+        results = await asyncio.gather(
+            *(core.stop() for core in reversed(self._cores)),
+            return_exceptions=True,
+        )
+        errors = [result for result in results if isinstance(result, BaseException)]
+        if errors:
+            raise errors[0]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def stop(self) -> None:
"""Stop all DP engine cores."""
"""Stop all data-parallel replica cores."""
await asyncio.gather(*(core.stop() for core in reversed(self._cores)))
async def stop(self) -> None:
"""Stop all data-parallel replica cores."""
results = await asyncio.gather(
*(core.stop() for core in reversed(self._cores)),
return_exceptions=True,
)
errors = [result for result in results if isinstance(result, BaseException)]
if errors:
raise errors[0]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/engine/async_engine.py` around lines 95 - 97, Update
AsyncEngine.stop to call asyncio.gather with return_exceptions=True, ensuring
every reversed self._cores replica completes its stop coroutine even when one
fails. Preserve propagation or handling of shutdown errors according to the
existing stop contract after all cores have finished.

Comment thread pypto_serving/serving/engine/output.py Outdated
Comment on lines +110 to +141
def process(
self,
scheduler_output: SchedulerOutput,
new_tokens: dict[str, int | list[int]],
) -> None:
request_outputs = self._scheduler.update_from_output(scheduler_output, new_tokens)

for request_output in request_outputs:
ctx = self._request_contexts.get(request_output.request_id)
if ctx is None:
continue

text = detokenize_incrementally(self._tokenizer, ctx)
self._detect_stop_string(request_output, ctx, text)

if request_output.finished:
text = finalize_detokenization(self._tokenizer, ctx)
self._schedule_worker_free(request_output.request_id)

if not ctx.stream and not request_output.finished:
continue

ctx.queue.put_nowait(
TokenOutput(
token_id=request_output.new_token_id,
text=text,
finished=request_output.finished,
finish_reason=request_output.finish_reason,
prompt_tokens=ctx.request.num_prompt_tokens,
completion_tokens=len(ctx.request.output_token_ids),
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

ReplicaEngineCore._request_contexts leaks on every normal/error completion. Both sites finish a request without removing its RequestContext from the shared _request_contexts dict; only abort_request's explicit .pop() and add_request's cancellation-only cleanup actually remove entries, so the two most common completion paths (success, worker error) leak for the life of the process.

  • pypto_serving/serving/engine/output.py#L110-L141: after request_output.finished is handled, add del self._request_contexts[request_output.request_id].
  • pypto_serving/serving/engine/replica.py#L509-L520: change self._request_contexts.get(request_id) to self._request_contexts.pop(request_id, None).
📍 Affects 2 files
  • pypto_serving/serving/engine/output.py#L110-L141 (this comment)
  • pypto_serving/serving/engine/replica.py#L509-L520
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/engine/output.py` around lines 110 - 141, Remove
completed request contexts from ReplicaEngineCore._request_contexts: in
pypto_serving/serving/engine/output.py lines 110-141, delete the entry after
request_output.finished handling; in pypto_serving/serving/engine/replica.py
lines 509-520, change the request lookup to pop(request_id, None) so
worker-error completion also cleans up safely.

Comment on lines +42 to +49
def map_finish_reason(reason: str) -> str:
mapping = {
"FINISHED_EOS": "stop",
"FINISHED_LENGTH": "length",
"FINISHED_STOP": "stop",
"FINISHED_ABORTED": "stop",
}
return mapping.get(reason, "stop")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unmapped "error" finish reason silently reported as "stop".

replica.py::_handle_step_error emits TokenOutput(finished=True, finish_reason="error") when the worker step fails, but "error" is absent from this mapping, so mapping.get(reason, "stop") returns "stop". Both collect_final_generation (non-streaming) and server.py's streaming paths use this mapping, so a backend failure mid-generation is silently reported to API clients as a normal, successful completion.

🐛 Proposed fix: surface backend errors distinctly
 def map_finish_reason(reason: str) -> str:
     mapping = {
         "FINISHED_EOS": "stop",
         "FINISHED_LENGTH": "length",
         "FINISHED_STOP": "stop",
         "FINISHED_ABORTED": "stop",
+        "error": "stop",
     }
-    return mapping.get(reason, "stop")
+    return mapping.get(reason, "stop")

A generic default preserves current behavior for genuinely unmapped reasons, but callers (e.g. collect_final_generation, the streaming handlers in server.py) should additionally check output.finish_reason == "error" and propagate a distinct error signal to the client (e.g. an HTTP 5xx for the non-streaming path, or an SSE error event/log for streaming) instead of masking it as "stop".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/server/responses.py` around lines 42 - 49, Handle the
backend failure reason emitted by replica.py::_handle_step_error before it is
normalized by map_finish_reason: update collect_final_generation and the
streaming handlers in server.py to detect output.finish_reason == "error" and
propagate a distinct client-visible failure, using an HTTP 5xx for non-streaming
responses and an SSE error event or equivalent logged streaming error. Keep
map_finish_reason’s existing default behavior for genuinely unmapped reasons.

Comment thread pypto_serving/serving/worker/service.py Outdated
Comment on lines +151 to +157
def busy_loop(self) -> None:
logger.info("Worker entering busy loop")
while True:
try:
raw: bytes = self.input_queue.get()
except Exception:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Log the exception before silently exiting the busy loop.

except Exception: break on the queue read discards the failure with no trace, unlike the other except blocks in this file. A worker that dies here leaves no diagnostic evidence, making a stalled replica hard to root-cause in production.

🪵 Proposed fix to log before breaking
             try:
                 raw: bytes = self.input_queue.get()
-            except Exception:
+            except Exception:
+                logger.error("Worker failed to read from input_queue", exc_info=True)
                 break
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def busy_loop(self) -> None:
logger.info("Worker entering busy loop")
while True:
try:
raw: bytes = self.input_queue.get()
except Exception:
break
def busy_loop(self) -> None:
logger.info("Worker entering busy loop")
while True:
try:
raw: bytes = self.input_queue.get()
except Exception:
logger.error("Worker failed to read from input_queue", exc_info=True)
break
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 156-156: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pypto_serving/serving/worker/service.py` around lines 151 - 157, Update the
exception handler in busy_loop so it logs the caught queue-read exception with
the existing logger before breaking out of the loop, matching the diagnostic
behavior of the other exception handlers in the file.

Source: Linters/SAST tools

Comment thread tests/test_kv_cache.py Outdated
Comment on lines +55 to +67
def test_grouped_cache_growth_is_atomic_across_groups() -> None:
manager = KvCacheManager(enable_prefix_cache=False)
manager.init_groups((_group("attention"), _group("state")), max_batch_size=1)
first = manager.ensure_group_blocks("first", token_count=4)

assert first == {"attention": [0, 1], "state": [0, 1]}
with pytest.raises(KVCacheCapacityError):
manager.ensure_group_blocks("second", token_count=2)
assert manager.group_request_partition("second") is None

manager.release_all_group_requests("first")
second = manager.ensure_group_blocks("second", token_count=2)
assert second == {"attention": [0], "state": [0]}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise a failure after one group has allocated.

first exhausts both identical two-block pools, so second fails in the first group and cannot verify rollback across groups. Give the groups asymmetric capacity and make a later group fail after an earlier group grows; then assert the earlier allocation was returned.

As per coding guidelines, maintain the repository’s unit tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_kv_cache.py` around lines 55 - 67, Update
test_grouped_cache_growth_is_atomic_across_groups to use asymmetric capacities
so ensure_group_blocks("second", ...) allocates from an earlier group before
failing in a later group. Assert the later failure leaves
group_request_partition("second") unset and the earlier group’s allocation
returned, then preserve the existing release and successful retry checks with
expectations adjusted to the asymmetric pool sizes.

Source: Coding guidelines

@superxf
superxf force-pushed the refactor_serving branch from ecf8abf to a9bb0f8 Compare July 27, 2026 11:10
@superxf
superxf merged commit a9bb0f8 into hw-native-sys:main Jul 28, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant