Skip to content

Add on-demand serving profile control - #127

Merged
bumble0918 merged 1 commit into
hw-native-sys:mainfrom
superxf:profile
Jul 30, 2026
Merged

bumble0918 merged 1 commit into
hw-native-sys:mainfrom
superxf:profile

Conversation

@superxf

@superxf superxf commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add opt-in, on-demand profiling control for HTTP serving. Profiling can now be
started and stopped without restarting the server, following the vLLM-style
/start_profile and /stop_profile workflow.

Interface changes

Serving CLI

HTTP serving profiling is now configured with command-line options:

Option Description
--profile Enable profiling support and expose the profile control endpoints.
--profile-output PATH Set the output directory or .json trace path. Defaults to ./profile_out.
--profile-level LEVELS Set comma-separated levels such as e2e,kernel or verbose. Defaults to e2e,kernel.

--profile-output and --profile-level require --profile.

This replaces SA_PROFILE_OUTPUT and SA_PROFILE_LEVEL for the HTTP serving
entry point. The environment variables remain supported by offline generation,
so the offline profiling interface is unchanged.

HTTP API

The following endpoints are registered when the server is launched with
--profile:

Method Endpoint Behavior
POST /start_profile Start recording in the API process and all replica workers.
POST /stop_profile Stop and flush all workers, merge the fragments into the final trace, and keep the server running.

Both endpoints return HTTP 200 with an empty response body on success. Repeated
start or stop requests are safe.

Profile commands are sent to every worker through IPC. The API waits for worker
acknowledgements before completing the request, so /stop_profile only returns
after worker-local fragments have been flushed and the merged trace has been
written.

Usage

pypto-serving \
  --model /path/to/model \
  --port 8225 \
  --profile \
  --profile-output /data/profile \
  --profile-level e2e,kernel
curl --noproxy "*" -X POST http://127.0.0.1:8225/start_profile

# Send the requests to profile.

curl --noproxy "*" -X POST http://127.0.0.1:8225/stop_profile

The merged trace is written to /data/profile/trace.json. Per-process JSONL
fragments remain under /data/profile/fragments/.

Validation

  • Added coverage for CLI configuration and environment-variable isolation.
  • Added recorder start/stop and fragment merge tests.
  • Added IPC, worker acknowledgement, replica coordination, and HTTP endpoint tests.
  • Profile and serving regression tests, Ruff, header checks, English-only checks,
    and git diff --check pass.

@coderabbitai

coderabbitai Bot commented Jul 30, 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: 7dd82c60-4ab8-4ed0-80b9-73ad32b66c11

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

On-demand profiling now uses CLI configuration and HTTP start/stop endpoints. Recorder activation is delayed until requested, worker processes acknowledge profile commands through IPC, engines coordinate replicas, and stopping merges fragments into the configured trace output.

Changes

HTTP profiling control

Layer / File(s) Summary
Profile configuration and recorder lifecycle
pypto_serving/tools/profile/*, tests/test_profile_control.py
Adds explicit profile configuration creation, delayed recorder activation, start/stop helpers, cleanup, and trace merging.
Worker profiling protocol
pypto_serving/serving/server/ipc.py, pypto_serving/serving/server/serving_worker.py, tests/test_profile_control.py
Adds profile commands, acknowledgements, dedicated queues, worker handling, and lifecycle cleanup.
Engine profile orchestration
pypto_serving/serving/engine/async_engine.py, tests/test_profile_control.py
Coordinates profile start and stop across replica cores, validates acknowledgements, and handles failures.
CLI and HTTP serving integration
pypto_serving/cli/main.py, pypto_serving/serving/server/server.py, tests/test_profile_control.py
Adds profiling CLI options and conditionally exposes HTTP endpoints that start, stop, and merge profiling sessions.
Operator workflow documentation
.agents/skills/qwen3-14b-online-perf-test/SKILL.md, docs/dev/profile.md
Documents CLI setup, endpoint sequencing, output paths, manual merging, troubleshooting, and profiling APIs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

A rabbit taps /start_profile bright,
Workers record through the night.
/stop_profile gathers the trace,
JSONL hops into place.
One neat profile, swift and light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: on-demand profiling control for serving.
Description check ✅ Passed The description matches the changeset and explains the new profiling CLI and HTTP control flow.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.agents/skills/qwen3-14b-online-perf-test/SKILL.md (1)

151-161: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid double-counting merged traces and retained fragments.

When trace.json exists, events() reads it and then unconditionally reads every retained fragment. Since the documentation states that fragments remain after merging, normal aggregation counts each event twice. Read fragments only when the merged file is absent, or select exactly one source.

Suggested fix
     if os.path.isfile(m):
         for e in json.load(open(m)).get("traceEvents",[]): yield e
-    for f in sorted(glob.glob(os.path.join(D,"fragments","trace.*.jsonl"))):
+        return
+    for f in sorted(glob.glob(os.path.join(D,"fragments","trace.*.jsonl"))):
🤖 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 @.agents/skills/qwen3-14b-online-perf-test/SKILL.md around lines 151 - 161,
The events() generator currently reads both the merged trace.json and retained
fragments, double-counting events; update it to read fragments only when
trace.json is absent, while preserving the existing parsing and error-skipping
behavior.
🧹 Nitpick comments (1)
pypto_serving/serving/server/serving_worker.py (1)

537-540: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant branch: profile_output_queue already defaults to None.

WorkerProcess.__init__ already accepts profile_output_queue: mp.Queue | None = None, so this conditional can collapse to a single call.

♻️ Simplify to a single call
-    if profile_output_queue is None:
-        worker = WorkerProcess(config, input_queue, output_queue)
-    else:
-        worker = WorkerProcess(config, input_queue, output_queue, profile_output_queue)
+    worker = WorkerProcess(config, input_queue, output_queue, profile_output_queue)
🤖 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/serving_worker.py` around lines 537 - 540,
Remove the redundant conditional around WorkerProcess construction and always
instantiate WorkerProcess with config, input_queue, output_queue, and
profile_output_queue; rely on WorkerProcess.__init__’s existing None default and
preserve the current behavior.
🤖 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 @.agents/skills/qwen3-14b-online-perf-test/SKILL.md:
- Around line 64-68: Move the /start_profile capture request after the /health,
/v1/models, and completion smoke-test readiness checks in the documented
workflow. Keep the existing startup-log readiness requirements, and ensure
profiling begins only after checklist steps 3–4 complete so those requests are
excluded from the trace.

In `@pypto_serving/serving/server/server.py`:
- Around line 160-171: Update _stop_profile so merge_profile() is always called
after engine.stop_profile(), including when it raises after partially stopping
replicas. Preserve the original exception by performing the merge in the failure
path before re-raising, and avoid merging twice on successful stops.

---

Outside diff comments:
In @.agents/skills/qwen3-14b-online-perf-test/SKILL.md:
- Around line 151-161: The events() generator currently reads both the merged
trace.json and retained fragments, double-counting events; update it to read
fragments only when trace.json is absent, while preserving the existing parsing
and error-skipping behavior.

---

Nitpick comments:
In `@pypto_serving/serving/server/serving_worker.py`:
- Around line 537-540: Remove the redundant conditional around WorkerProcess
construction and always instantiate WorkerProcess with config, input_queue,
output_queue, and profile_output_queue; rely on WorkerProcess.__init__’s
existing None default and preserve the current behavior.
🪄 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: a52e8fd7-6021-412e-9d7e-ec2ebbe451cb

📥 Commits

Reviewing files that changed from the base of the PR and between 18c7284 and d63576f.

📒 Files selected for processing (11)
  • .agents/skills/qwen3-14b-online-perf-test/SKILL.md
  • docs/dev/profile.md
  • pypto_serving/cli/main.py
  • pypto_serving/serving/engine/async_engine.py
  • pypto_serving/serving/server/ipc.py
  • pypto_serving/serving/server/server.py
  • pypto_serving/serving/server/serving_worker.py
  • pypto_serving/tools/profile/__init__.py
  • pypto_serving/tools/profile/env.py
  • pypto_serving/tools/profile/recorder.py
  • tests/test_profile_control.py

Comment on lines +64 to +68
Wait for `INFO: Application startup complete.` / `Uvicorn running on http://0.0.0.0:<port>` before sending traffic. The worker prints `Worker entering busy loop` and the engine prints `Engine loop started` once the model and KV cache are ready. Then start the capture:

```bash
curl --noproxy "*" -sf -X POST http://localhost:$PORT/start_profile
```

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

Run readiness checks before starting the capture.

/start_profile is called before the subsequent /health, /v1/models, and completion smoke test, so those readiness requests contaminate the profiling trace. Move this call after the checks, matching checklist steps 3–4.

🤖 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 @.agents/skills/qwen3-14b-online-perf-test/SKILL.md around lines 64 - 68,
Move the /start_profile capture request after the /health, /v1/models, and
completion smoke-test readiness checks in the documented workflow. Keep the
existing startup-log readiness requirements, and ensure profiling begins only
after checklist steps 3–4 complete so those requests are excluded from the
trace.

Comment on lines +160 to +171
async def _stop_profile(self) -> Response:
async with self._profile_lock:
logger.info("Stopping SA profiler...")
try:
await self.engine.stop_profile()
except Exception:
stop_sa_profile()
raise
stop_sa_profile()
event_count = merge_profile()
logger.info("SA profiler stopped; merged %d events", event_count)
return Response(status_code=200)

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

Merge is skipped when engine.stop_profile() partially fails, stranding already-flushed fragments.

AsyncLLMEngine.stop_profile() always attempts core.stop_profile() on every replica (via gather(return_exceptions=True)) before raising, so most/all workers will have already flushed their fragment files even when this raises. But the except branch here re-raises without calling merge_profile(), so a partial failure leaves flushed data unmerged until the caller happens to retry /stop_profile.

🛠️ Always merge on stop, even if a worker's ack failed
     async def _stop_profile(self) -> Response:
         async with self._profile_lock:
             logger.info("Stopping SA profiler...")
             try:
                 await self.engine.stop_profile()
-            except Exception:
-                stop_sa_profile()
-                raise
-            stop_sa_profile()
-            event_count = merge_profile()
-            logger.info("SA profiler stopped; merged %d events", event_count)
+            finally:
+                stop_sa_profile()
+                event_count = merge_profile()
+                logger.info("SA profiler stopped; merged %d events", event_count)
         return Response(status_code=200)
📝 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_profile(self) -> Response:
async with self._profile_lock:
logger.info("Stopping SA profiler...")
try:
await self.engine.stop_profile()
except Exception:
stop_sa_profile()
raise
stop_sa_profile()
event_count = merge_profile()
logger.info("SA profiler stopped; merged %d events", event_count)
return Response(status_code=200)
async def _stop_profile(self) -> Response:
async with self._profile_lock:
logger.info("Stopping SA profiler...")
try:
await self.engine.stop_profile()
finally:
stop_sa_profile()
event_count = merge_profile()
logger.info("SA profiler stopped; merged %d events", event_count)
return Response(status_code=200)
🤖 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/server.py` around lines 160 - 171, Update
_stop_profile so merge_profile() is always called after engine.stop_profile(),
including when it raises after partially stopping replicas. Preserve the
original exception by performing the merge in the failure path before
re-raising, and avoid merging twice on successful stops.

@bumble0918

Copy link
Copy Markdown
Collaborator

HTTP serving 检测到 SA_PROFILE_* 但未传 --profile 时给个deprecated 的 warning? 避免用户按旧方式启动以为 profiling 开了实际没开

@bumble0918
bumble0918 merged commit c96c471 into hw-native-sys:main Jul 30, 2026
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.

2 participants