Skip to content

[https://nvbugs/6472256][fix] Fix disagg stress cluster flapping and DeepSeek R1 FP4 ctx OOM; add aiperf error-rate gate - #17427

Open
moraxu wants to merge 1 commit into
NVIDIA:mainfrom
moraxu:dev-mguzek-nvbug-6472256-disagg-stress-test
Open

[https://nvbugs/6472256][fix] Fix disagg stress cluster flapping and DeepSeek R1 FP4 ctx OOM; add aiperf error-rate gate#17427
moraxu wants to merge 1 commit into
NVIDIA:mainfrom
moraxu:dev-mguzek-nvbug-6472256-disagg-stress-test

Conversation

@moraxu

@moraxu moraxu commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Dev Engineer Review

  • Updated disaggregated stress runners to use production discovery timings.
  • Added configurable cluster timing overrides and fatal-log context reporting.
  • Added enforce_aiperf_error_rate() with cancellation-aware error handling.
  • Reduced DeepSeek R1 FP4 context-server max_num_tokens to 8320 to reduce transient TRTLLM-Gen workspace OOM risk.
  • Removed the B200 waiver for the affected stress configuration.
  • The changes are consistent with the stated objectives. No configuration typos or unintended scope changes are reported.

QA Engineer Review

  • Modified test code in tests/integration/defs/disaggregated/test_disaggregated.py.
  • Modified the DeepSeek R1 FP4 integration configuration.
  • Removed the corresponding B200 waiver from tests/integration/test_lists/waives.txt.
  • The affected stress test is now unwaived.
  • Validation reports 35,000 requests with 0 errors, 0 OOMs, and GSM8K accuracy of 0.9568.
  • Verdict: sufficient.

Description

Root cause & fix (two independent issues).

  1. The reported failure ("Cluster is not ready" 500-storm, 2026-07-17 run): spurious cluster-membership flapping. The CI harness injected heartbeat_interval_sec=1 / inactive_timeout_sec=2 (production defaults: 5/10) while the worker heartbeat task, the cluster-storage /expire handler, and the expiry sweep all share asyncio event loops saturated at 512-concurrency; any >1s stall spuriously expired a worker, flipping is_ready() false, and closed-loop clients amplified each window into thousands of fast 500s. Since [https://nvbugs/6440089][test] Rework disagg mixed-stress accuracy metric and harden the test #16445 removed the log patterns and the test was waived, a recurrence would have passed silently (the aiperf path had no error-rate gate). Fix from this PR: stress runners now use production discovery timings; a non-cancellation error-rate gate on aiperf's per-record export restores coverage; test unwaived.

  2. Found while validating: deterministic ctx-worker CUDA OOM on current main. Bisect across rc21/rc22/rc23/rc24 wheels, 4 targeted commit reverts, and era-matched deps (13 probes) proved it is not a code regression: the config is memory-borderline on 192GB B200 — weights 107.5 GiB/GPU + KV pool (0.8 fraction) + ~7 GiB CUDA/NCCL leave less headroom than real serving transients (~15–18 GiB, dominated by a 6.3–8 GiB TRTLLM-Gen FP4 MoE workspace at 16640-token chunks), while KV-pool sizing only observes a 10.77 GiB dynamic peak during estimation. Shortfall at OOM: 1–4 GiB; the 2026-07-17 pass was environment margin. Fix from this PR:: ctx moe_config.max_num_tokens: 8320 halves the MoE transient. Validation: full 35,000-request stress run passed — 0 errors, 0 OOM, GSM8K 0.9568.

Test Coverage

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

Signed-off-by: Michal Guzek <mguzek@nvidia.com>
@moraxu

moraxu commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The disaggregated tests now use production service-discovery timings, support cluster overrides, show fatal-log context, and enforce aiperf error-rate limits. The FP4 configuration limits context-server tokens, and its stress-test waiver is removed.

Changes

Disaggregated test reliability

Layer / File(s) Summary
Production discovery settings
tests/integration/defs/disaggregated/test_disaggregated.py
Cluster setup accepts service-discovery overrides. Stress and mixed-stress paths use 5-second heartbeats and a 10-second inactive timeout.
Aiperf validation and diagnostics
tests/integration/defs/disaggregated/test_disaggregated.py
Aiperf artifacts are checked against a configurable error-rate threshold. Intentional cancellations are excluded. Fatal-log output includes nearby context.
FP4 stress readiness
tests/integration/defs/disaggregated/test_configs/disagg_config_ctxtp4_gentp4_deepseek_r1_v2_fp4_tllm.yaml, tests/integration/defs/disaggregated/test_disaggregated.py, tests/integration/test_lists/waives.txt
The FP4 context server sets max_num_tokens to 8320. The KV-overflow regression disables error-rate enforcement. The B200 stress-test waiver is removed.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant run_disaggregated_aiperf
  participant fatal_log_scanner
  participant enforce_aiperf_error_rate
  run_disaggregated_aiperf->>fatal_log_scanner: scan saved logs
  fatal_log_scanner-->>run_disaggregated_aiperf: return first fatal-log context
  run_disaggregated_aiperf->>enforce_aiperf_error_rate: validate request errors
  enforce_aiperf_error_rate-->>run_disaggregated_aiperf: pass or raise threshold failure
Loading

Possibly related PRs

Suggested reviewers: brnguyen2

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required NVBugs and type format and clearly summarizes the cluster-flapping, OOM, and error-rate-gate fixes.
Description check ✅ Passed The description explains both root causes, fixes, and validation results, although the dedicated Test Coverage section is not separately populated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/integration/defs/disaggregated/test_disaggregated.py`:
- Around line 2385-2389: Replace the assert checking export_path in the
disaggregated test with an explicit FileNotFoundError, preserving the existing
diagnostic message and path context so missing exports fail immediately
regardless of Python optimization.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 11b7339f-4141-4c3e-a8b6-0b61bafffe4e

📥 Commits

Reviewing files that changed from the base of the PR and between 81db688 and e5a3c90.

📒 Files selected for processing (3)
  • tests/integration/defs/disaggregated/test_configs/disagg_config_ctxtp4_gentp4_deepseek_r1_v2_fp4_tllm.yaml
  • tests/integration/defs/disaggregated/test_disaggregated.py
  • tests/integration/test_lists/waives.txt
💤 Files with no reviewable changes (1)
  • tests/integration/test_lists/waives.txt

Comment on lines +2385 to +2389
export_path = os.path.join(artifact_dir, "profile_export.jsonl")
assert os.path.exists(export_path), (
f"aiperf per-record export not found at {export_path}; cannot enforce "
"the request error-rate gate. If this aiperf version/export level does "
"not produce it, pass max_error_rate=None explicitly.")

Copy link
Copy Markdown
Contributor

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

Replace the assertion with an explicit exception.

Python removes assert statements when it runs with -O. A missing export then fails later at open() without the intended diagnostic. Raise FileNotFoundError explicitly.

Proposed fix
-    assert os.path.exists(export_path), (
-        f"aiperf per-record export not found at {export_path}; cannot enforce "
-        "the request error-rate gate. If this aiperf version/export level does "
-        "not produce it, pass max_error_rate=None explicitly.")
+    if not os.path.exists(export_path):
+        raise FileNotFoundError(
+            f"aiperf per-record export not found at {export_path}; cannot enforce "
+            "the request error-rate gate. If this aiperf version/export level does "
+            "not produce it, pass max_error_rate=None explicitly.")

As per coding guidelines, “raise ValueError rather than assertions.”

📝 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
export_path = os.path.join(artifact_dir, "profile_export.jsonl")
assert os.path.exists(export_path), (
f"aiperf per-record export not found at {export_path}; cannot enforce "
"the request error-rate gate. If this aiperf version/export level does "
"not produce it, pass max_error_rate=None explicitly.")
export_path = os.path.join(artifact_dir, "profile_export.jsonl")
if not os.path.exists(export_path):
raise FileNotFoundError(
f"aiperf per-record export not found at {export_path}; cannot enforce "
"the request error-rate gate. If this aiperf version/export level does "
"not produce it, pass max_error_rate=None explicitly.")
🤖 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/integration/defs/disaggregated/test_disaggregated.py` around lines 2385
- 2389, Replace the assert checking export_path in the disaggregated test with
an explicit FileNotFoundError, preserving the existing diagnostic message and
path context so missing exports fail immediately regardless of Python
optimization.

Source: Coding guidelines

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64668 [ run ] triggered by Bot. Commit: e5a3c90 Link to invocation

@brnguyen2 brnguyen2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The root-cause write-up is excellent and the cluster-timing fix looks right — the harness was the only thing injecting 1s/2s, and every stress path now goes through PRODUCTION_CLUSTER_TIMINGS.

Two scope concerns worth settling before merge:

The error-rate gate is on by default for every stress param, but only one was validated. max_error_rate=0.05 now applies to all nine test_disaggregated_stress_test configs (gpt-oss ×4, GLM-5, Qwen3.5-4B, Qwen3-32B, DeepSeek MTP) plus the unwaived non-MTP DeepSeek run, and all of them pass cancellation_rate=10. If aiperf classifies a cancelled request as anything other than code == 499 / type == "RequestCancellationError" — no error object at all, or a different type string — 10% of requests land in the numerator and every stress test fails at once. Please paste the [aiperf-gate] line from the 35k validation run so the cancelled: count can be confirmed at ~3500, and pin the aiperf version this record schema comes from. Also worth noting the gate has never been observed firing; a small unit test feeding enforce_aiperf_error_rate a synthetic profile_export.jsonl (clean / all-cancelled / 500-storm) would prove it catches the failure it exists for, and costs no GPU.

The two DeepSeek configs have diverged. disagg_config_ctxtp4_gentp4_deepseek_r1_v2_fp4_tllm_mtp.yaml has the same ctx sizing (max_num_tokens: 16640, 0.8 KV fraction, TRTLLM MoE) and does not get the chunking. If the analysis holds it is exposed to the same transient, and it is not waived.

Description hygiene: the description covers the diff accurately; test-only change, so no docs/changelog owed.

[0, error.get("message", "")])
entry[0] += 1
considered = total - cancelled
if considered <= 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This early return makes the gate silently pass in the cases you most want it to fail. total only counts lines that parsed as JSON, so if aiperf writes an empty export, gets killed mid-write, or changes the export path/nesting in a future version, total == 0considered <= 0 → clean pass. Same for the json.JSONDecodeError: continue above: a wholesale format change degrades to "no errors found" instead of an error.

Suggest counting decode failures and asserting the export is substantive before computing the rate:

assert total > 0, f"{export_path} contained no parseable records ({malformed} malformed lines)"
assert malformed <= 0.01 * (total + malformed), ...
if considered <= 0:
    raise AssertionError(f"all {total} records were cancellations")

code = error.get("code")
err_type = error.get("type")
if code == 499 or err_type == "RequestCancellationError":
cancelled += 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The whole gate hinges on this classification being exhaustive: these stress runs use --request-cancellation-rate 10, so if aiperf reports a cancelled request under any other shape (a different type string, a null code, or no error object with a separate status field), ~10% of records become "errors" and every stress test trips the 5% threshold on the first run.

Please record the aiperf version this record schema was validated against in the docstring, and post the [aiperf-gate] output line from the 35k validation run so the cancelled: count can be checked against the expected ~3500.

# KV pool on 192GB B200 and intermittently OOMs the ctx worker under
# 512-concurrency 8k prefill load (memory estimation only observes
# ~10.8 GiB dynamic peak, so the KV pool leaves no slack for it).
max_num_tokens: 8320

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

disagg_config_ctxtp4_gentp4_deepseek_r1_v2_fp4_tllm_mtp.yaml has an identical context server (max_num_tokens: 16640, free_gpu_memory_fraction: 0.8, TRTLLM MoE backend, same weights) and does not get this chunking. By the analysis in the description it carries the same workspace transient, and deepseek_r1_v2_fp4_mtp_stress is not waived — so it is a live OOM candidate on B200. Either apply the same moe_config.max_num_tokens there or say in the PR why the MTP variant's headroom differs (enable_attention_dp: false and max_draft_len: 1 change the picture, but in which direction?).

full:B200/accuracy/test_llm_api_pytorch.py::TestQwen3_30B_A3B::test_dummy_load_format SKIP (https://nvbugs/6525059)
full:B200/disaggregated/test_disaggregated.py::test_disaggregated_stress_test[input8k-output1k-conc512-deepseek_r1_v2_fp4_stress] SKIP (https://nvbugs/6472256)
full:B200/disaggregated/test_disaggregated.py::test_disaggregated_stress_test[input8k-output1k-conc512-glm5_nvfp4_tp4_ep4_dp_stress] SKIP (https://nvbugs/6544407)
full:B200/disaggregated/test_disaggregated.py::test_disaggregated_stress_test[input8k-output1k-conc512-qwen3_32b_fp8_stress] SKIP (https://nvbugs/6472256)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

qwen3_32b_fp8_stress is still waived under the same NVBug this PR fixes. The cluster-timing fix applies to it too (it runs through the same run_disaggregated_aiperf path), so either it should be unwaived alongside the DeepSeek param, or — if it fails for an unrelated reason — it needs its own bug ID. As it stands, nvbugs/6472256 can't be closed by this PR.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64668 [ run ] completed with state ABORTED. Commit: e5a3c90

Link to invocation

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.

3 participants