Skip to content

feat(sdk): model PP stage imbalance and pipe fill instead of ideal linear speedup - #1459

Draft
tianhaox wants to merge 5 commits into
ai-dynamo:mainfrom
tianhaox:feat/pp-schedule-modeling
Draft

feat(sdk): model PP stage imbalance and pipe fill instead of ideal linear speedup#1459
tianhaox wants to merge 5 commits into
ai-dynamo:mainfrom
tianhaox:feat/pp-schedule-modeling

Conversation

@tianhaox

@tianhaox tianhaox commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Overview:

PP was modeled as an ideal linear speedup. A step's latency was the whole
model's (_num_layers is never divided by pp_size) and throughput was simply
multiplied by pp_size. That is exact only when every stage costs the same and
the pipe is always full — neither holds.

This adds PipelineSchedule, which computes the two factors that were missing:
stage imbalance and pipe fill. pp_size == 1 is bit-identical to before.

Details:

What was wrong

A pipeline advances at max_i(t_i), not the average. The un-sharded lm_head
sits alone on the last stage, so for a decode step it sets the cycle.
Qwen3-32B (64 layers, vocab 151936) on H20 at pp=8:

stage times (ms): [3.153, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.901]
                                                                    ^ lm_head
ideal cycle 3.244   realized 3.901   ->  balance 83.2%  ->  6.65x, not 8x

Prefill steps stay at 99.9% — per-layer work dominates there, so this is a
decode-side effect.

Two more gaps: num_layers % pp_size != 0 only logged "we're nothing to
correct this"
while the fattest stage actually gates the cycle (64 layers over
6 stages costs 11/(64/6)), and a starved pipe still scaled linearly.

The model

cycle_time     = max_i(stage_time_i) + p2p_per_hop
balance_factor = (step_total / pp_size) / cycle_time   # per-microbatch latency effect
fill_factor    = min(1, num_microbatches / pp_size)    # throughput-only effect

Op placement reuses the naming contract the model classes already require
(GPTModel docstring: "Same for logits_gemm"): *embedding* → first stage,
*logits_gemm* → last, *p2p* → link (charged once per hop, not as a
whole-step total), everything else per-layer. warn_on_unclassified_ops flags a
new head-like op that would otherwise be silently smeared across every stage.

Layer partition defaults to an even split with the remainder on the leading
stages (vLLM get_pp_indices / TRT-LLM); an explicit partition is accepted.

Where it plugs in — two touch points in run_agg:

mix_step_latency_ms     /= pp_schedule.balance_factor(mix_per_ops, model._num_layers)
genonly_step_latency_ms /= pp_schedule.balance_factor(genonly_per_ops, model._num_layers)
output_throughput = output_throughput * scale_factor * pp_schedule.fill_factor()

Inflating the step latency to the real traversal time (pp * cycle) is the
load-bearing choice: TTFT, TPOT, _total_step_latency_ms, _step_throughput
and _throughput_cap all derive from it, so they stay consistent without
individual patches.

Why orchestration and not the op layer — per .claude/rules/rust-core/parity.md,
changing op query math obligates a mirrored Rust implementation plus oracle
anchors. This consumes the per-op breakdown after the step is evaluated, so
both engine-step paths benefit with no parity surface touched. Same layering
AFD's pipeline model already uses.

Measured effect (Qwen3-32B, 8x H20, isl=4000/osl=500/bs=32 — prefill-dominated,
so the correction is mild here; decode-heavy workloads move much more):

tp/pp tokens/s/gpu before → after
8/1 207.72 → 207.72 (unchanged)
4/2 268.37 → 266.78
2/4 309.94 → 305.85
1/8 344.44 → 333.21

Where should the reviewer start?

  • aic-core/src/aiconfigurator_core/sdk/pipeline.py — the whole model, ~200 lines
  • aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py — two touch
    points in run_agg plus the _pipeline_schedule backend hook
  • docs/PIPELINE_PARALLEL_MODELING.md — derivation and the known-gaps list

Testing

  • New tests/unit/sdk/test_pipeline_schedule.py: 40 cases (placement, partition
    incl. uneven splits, max-not-average cycle, P2P hop amortization + overlap,
    starvation, the pp_size == 1 identity, validation).
  • Full tests/unit: 2974 passed. The 3 failures in
    tests/unit/collector/sglang/test_collect_mla_module.py (GLM-5 checkpoint
    naming) are pre-existing — reproduced on a clean baseline.
  • pp_size == 1 verified bit-identical end to end on H20: TTFT 650.722 /
    TPOT 18.195 / 207.72 tokens/s/gpu, unchanged.

Known gaps (documented in the design doc, deliberately not in this PR)

  1. fill_factor is linear — the real curve is likely steeper once scheduler
    and sync overhead are counted. Needs silicon calibration.
  2. Chunked prefill x PP is not modeled — adjacent chunks of one request have
    a RAW dependency on the KV they write, so they cannot occupy the pipe
    simultaneously. fill_factor is the intended hook; the coefficient needs
    measurement.
  3. P2P still hardcodes inter_node_bw (~9x pessimistic intra-node).
    SystemSpec.get_p2p_bandwidth(num_gpus) already implements the topology
    tiers on both sides, but the correct selector is tp * pp, which the op is
    not constructed with — fixing it touches every model's P2P construction
    plus the Rust mirror, so it belongs in its own change. Impact at pp=8:
    0.67% of a decode step, 2.2% of a prefill step, conservative direction.
  4. PP is still excluded from the automatic searchbuild_disagg_parallel_lists
    takes should_enable_pp (default False) and no caller passes True.
    Turning it on should wait until 1 and 2 are calibrated.

Related Issues:

  • Relates to: PP modeling alignment (no tracked issue yet)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added pipeline-parallel performance modeling with configurable stage placement, layer partitioning, and microbatch scheduling.
    • Estimates now account for stage balance, communication overhead, pipeline fill, and overlap efficiency.
    • Added diagnostics for incomplete or potentially misclassified pipeline operations.
  • Bug Fixes

    • Improved latency and throughput estimates for multi-stage pipelines while preserving single-stage behavior.
  • Documentation

    • Added guidance on pipeline modeling, configuration, integration, and known limitations.

…near speedup

PP was modeled as an ideal linear speedup: a step's latency was the whole
model's (`_num_layers` is never divided by `pp_size`) and throughput was simply
multiplied by `pp_size`. That is exact only when every stage costs the same and
the pipe is always full.

A pipeline advances at `max_i(t_i)`, not the average. The un-sharded `lm_head`
sits alone on the last stage, so for a decode step it sets the cycle: Qwen3-32B
on H20 at pp=8 runs at 83.2% of the ideal cycle (6.65x, not 8x). Prefill steps
stay near-ideal at 99.9% because per-layer work dominates there. Uneven layer
splits previously only produced a warning saying "we're nothing to correct
this", and a starved pipe still scaled linearly.

Add `PipelineSchedule`, which folds the step model's existing per-op breakdown
into per-stage times and returns:

  balance_factor = (step_total / pp_size) / cycle_time   # latency effect
  fill_factor    = min(1, num_microbatches / pp_size)    # throughput effect

`run_agg` inflates the mix/genonly step latencies by 1/balance_factor to get
the real traversal time, so TTFT, TPOT and throughput all follow consistently
downstream, and charges fill_factor against throughput only.

This is orchestration, not op query math, so the Rust engine-step parity
surface is untouched and both engines benefit -- the same layering AFD's
pipeline model already uses.

`pp_size == 1` returns exactly 1.0 from both factors, so single-stage results
are bit-identical.

Known gaps are documented in docs/PIPELINE_PARALLEL_MODELING.md: fill_factor
needs silicon calibration, chunked-prefill x PP is not yet modeled, the P2P op
still hardcodes inter_node_bw, and PP remains excluded from the automatic
search.

Signed-off-by: tianhaox <tianhaox@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the feat label Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e9033622-fc3b-42f7-9ba0-ae7e0329535a

📥 Commits

Reviewing files that changed from the base of the PR and between d1d1eef and dfe11fc.

📒 Files selected for processing (1)
  • docs/PIPELINE_PARALLEL_MODELING.md
📜 Recent review details
⏰ Context from checks skipped due to timeout. (10)
  • GitHub Check: Collect snapshot (new)
  • GitHub Check: Collect snapshot (old)
  • GitHub Check: Build wheels (manylinux_2_28_x86_64)
  • GitHub Check: Build wheels (macosx_arm64)
  • GitHub Check: Build wheels (manylinux_2_28_aarch64)
  • GitHub Check: aic-core public API contract
  • GitHub Check: Build and Test (unit)
  • GitHub Check: Rust/Python engine-step parity
  • GitHub Check: Build and Test (e2e)
  • GitHub Check: Cargo Deny
🧰 Additional context used
📓 Path-based instructions (2)
**/*

⚙️ CodeRabbit configuration file

**/*: - Prefer applicable inline comments. When the correct fix is clear, small, and limited to the commented diff hunk, include it as a GitHub Suggested Change so the author can apply it with one click.

  • Do not use a suggested change when the fix requires broader design choices, multiple files, generated artifacts, unavailable context, or validation that cannot be inferred from the diff.
  • If a comment is not directly applicable, state the smallest concrete next step and why a one-click suggestion is not safe.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
docs/**

⚙️ CodeRabbit configuration file

docs/**: - Check that docs match changed CLI, SDK, generator, backend, and support-matrix behavior.

  • Flag docs that describe unsupported runtimes, stale command names, or behavior not covered by tests or support-matrix evidence.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
🔇 Additional comments (4)
docs/PIPELINE_PARALLEL_MODELING.md (4)

161-198: LGTM!


210-251: LGTM!


253-335: LGTM!


336-351: LGTM!


Walkthrough

Pipeline parallel modeling now includes stage placement, layer partitioning, steady-state scheduling, P2P costs, balance and fill factors, backend integration, diagnostics, tests, and documentation.

Changes

Pipeline Parallel Modeling

Layer / File(s) Summary
Schedule configuration and stage modeling
aic-core/src/aiconfigurator_core/sdk/pipeline.py, tests/unit/sdk/test_pipeline_modeling.py
Adds operation placement, layer partitioning, validated PipelineLayout configuration, stage-time aggregation, and P2P latency calculation.
Pipeline timing and efficiency calculations
aic-core/src/aiconfigurator_core/sdk/pipeline.py, tests/unit/sdk/test_pipeline_modeling.py
Adds PipelineSteadyState cycle-time, fill, balance, efficiency, diagnostics, and unclassified-operation warnings.
Backend latency and throughput integration
aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py, docs/PIPELINE_PARALLEL_MODELING.md
Adds backend hooks, cache-key microbatch settings, pipeline-aware latency correction, fill-adjusted throughput scaling, and model documentation.

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

Poem

Stages divide the layers bright,
Microbatches flow through measured flight.
Balance, fill, and cycles align,
Backend throughput follows the line.
Single-stage paths remain benign.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change from ideal linear PP speedup to stage-imbalance and pipe-fill modeling.
Description check ✅ Passed The description covers the required overview, implementation details, reviewer starting points, testing, known gaps, and a related-issues entry.
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.

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

🤖 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 `@aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py`:
- Around line 1533-1537: The PP correction currently derives from Rust aggregate
synthetic operations, losing placement-specific embedding, logits, and P2P
costs. Update the schedule calculation around _pipeline_schedule, run_mixed, and
_get_genonly_step_latency to use a placement-preserving per-operation breakdown,
or an equivalent Python-only breakdown solely for balance computation, while
preserving Rust aggregate results for execution. Add integration coverage with
pp_size > 1 for both engine-step backends and verify equivalent corrections.
- Around line 1522-1537: Separate pipeline traversal latency from steady-state
cycle time: in base_backend.py lines 1522-1537, update the backend latency
calculations so TTFT and request TPOT use stage traversal time, while throughput
alone uses the balance-adjusted steady-state cycle. In
aic-core/src/aiconfigurator_core/sdk/pipeline.py lines 161-165, apply P2P
overhead only across actual inter-stage edges and omit the final-stage hop. In
tests/unit/sdk/test_pipeline_schedule.py lines 169-176, add coverage for a P2P
schedule with a last-stage bottleneck and assert distinct traversal and
throughput-cycle values. In docs/PIPELINE_PARALLEL_MODELING.md lines 110-117,
document the corrected traversal-versus-cycle semantics instead of universally
equating traversal with pp times cycle.
- Around line 126-128: Update the warning logic in the scheduling method around
warn_on_unclassified_ops so it inspects both model.context_ops and
model.generation_ops when mixed-step scheduling is active. Ensure unclassified
operations from either pipeline are included in the placement warning while
preserving the existing layer-count and schedule behavior.

In `@aic-core/src/aiconfigurator_core/sdk/pipeline.py`:
- Around line 120-121: Update layer_partition to validate an explicitly
configured self.partition before returning it: reject negative layer counts and
partitions whose sum differs from num_layers, while preserving
even_partition(num_layers, self.pp_size) for unset partitions. Ensure validation
remains compatible with generator-provided partition inputs.

In `@docs/PIPELINE_PARALLEL_MODELING.md`:
- Line 41: Add the appropriate language identifier, such as text, to both fenced
code blocks in the pipeline parallel modeling documentation, including the
blocks containing stage timing output and the balance_factor expression, while
preserving their contents.

In `@tests/unit/sdk/test_pipeline_schedule.py`:
- Around line 16-26: Update the import block in test_pipeline_schedule.py using
the repository-configured Ruff ordering, equivalent to running ruff check --fix
on the file. Preserve the imported symbols and commit the resulting
section/order changes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 733fa9f1-2b8d-4a11-abc3-0017562323f6

📥 Commits

Reviewing files that changed from the base of the PR and between eed87c1 and a47383c.

📒 Files selected for processing (4)
  • aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
  • docs/PIPELINE_PARALLEL_MODELING.md
  • tests/unit/sdk/test_pipeline_schedule.py
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
  • GitHub Check: Collect snapshot (new)
  • GitHub Check: Collect snapshot (old)
  • GitHub Check: Build wheels (manylinux_2_28_x86_64)
  • GitHub Check: Build wheels (macosx_arm64)
  • GitHub Check: Build wheels (manylinux_2_28_aarch64)
  • GitHub Check: Build and Test (unit)
  • GitHub Check: Rust/Python engine-step parity
  • GitHub Check: Cargo Deny
  • GitHub Check: Build and Test (e2e)
  • GitHub Check: aic-core public API contract
🧰 Additional context used
📓 Path-based instructions (4)
**/*

⚙️ CodeRabbit configuration file

**/*: - Prefer applicable inline comments. When the correct fix is clear, small, and limited to the commented diff hunk, include it as a GitHub Suggested Change so the author can apply it with one click.

  • Do not use a suggested change when the fix requires broader design choices, multiple files, generated artifacts, unavailable context, or validation that cannot be inferred from the diff.
  • If a comment is not directly applicable, state the smallest concrete next step and why a one-click suggestion is not safe.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
  • tests/unit/sdk/test_pipeline_schedule.py
  • aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
docs/**

⚙️ CodeRabbit configuration file

docs/**: - Check that docs match changed CLI, SDK, generator, backend, and support-matrix behavior.

  • Flag docs that describe unsupported runtimes, stale command names, or behavior not covered by tests or support-matrix evidence.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
aic-core/src/aiconfigurator_core/sdk/**

⚙️ CodeRabbit configuration file

aic-core/src/aiconfigurator_core/sdk/**: - Verify core SDK API changes remain compatible with legacy aiconfigurator.sdk imports, generator inputs, profiler data flow, and documented examples.

  • Flag upper-layer dependencies or silent schema drift introduced into the minimal core distribution.

Files:

  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
  • aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
tests/**

⚙️ CodeRabbit configuration file

tests/**: - Check that tests cover the changed behavior rather than only the happy path.

  • Watch for fixtures or golden outputs that mask backend drift, support-matrix ordering changes, or CLI output regressions.

Files:

  • tests/unit/sdk/test_pipeline_schedule.py
🪛 GitHub Actions: Lint and Format / 1_Lint and Format (Ruff).txt
tests/unit/sdk/test_pipeline_schedule.py

[error] 16-26: Ruff I001: Import block is unsorted or unformatted. Run 'ruff check --fix .' to organize imports.

🪛 GitHub Actions: Lint and Format / Lint and Format (Ruff)
tests/unit/sdk/test_pipeline_schedule.py

[error] 16-26: Ruff I001: Import block is unsorted or unformatted. Organize the imports or run 'ruff check --fix'. Command failed with exit code 1.

🪛 markdownlint-cli2 (0.23.1)
docs/PIPELINE_PARALLEL_MODELING.md

[warning] 41-41: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 71-71: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (1)
aic-core/src/aiconfigurator_core/sdk/pipeline.py (1)

85-105: 🗄️ Data Integrity & Integration

Keep PipelineSchedule core-only.

The legacy backend aliases the core backend, and documentation and tests use aiconfigurator_core.sdk.pipeline.PipelineSchedule. No legacy re-export is required.

Comment on lines +126 to +128
if pp_size > 1:
warn_on_unclassified_ops(model.generation_ops, model._num_layers)
return schedule

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

Inspect context operations for unclassified placement.

Mixed-step scheduling consumes context and generation operation latency. This warning only inspects model.generation_ops, so a low-scale unmarked context operation can still be silently distributed across all stages.

Proposed fix
 if pp_size > 1:
-    warn_on_unclassified_ops(model.generation_ops, model._num_layers)
+    warn_on_unclassified_ops([*model.context_ops, *model.generation_ops], model._num_layers)

As per path instructions, “Model operation pipelines are defined through context_ops and generation_ops.”

📝 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
if pp_size > 1:
warn_on_unclassified_ops(model.generation_ops, model._num_layers)
return schedule
if pp_size > 1:
warn_on_unclassified_ops([*model.context_ops, *model.generation_ops], model._num_layers)
return schedule
🤖 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 `@aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py` around lines
126 - 128, Update the warning logic in the scheduling method around
warn_on_unclassified_ops so it inspects both model.context_ops and
model.generation_ops when mixed-step scheduling is active. Ensure unclassified
operations from either pipeline are included in the placement warning while
preserving the existing layer-count and schedule behavior.

Source: Path instructions

Comment thread aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
Comment thread aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py Outdated
Comment on lines +120 to +121
def layer_partition(self, num_layers: int) -> tuple[int, ...]:
return self.partition if self.partition is not None else even_partition(num_layers, self.pp_size)

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 | 🟠 Major | ⚡ Quick win

Validate explicit partition contents.

Line 121 accepts negative partitions and partitions whose sum differs from num_layers. These values produce invalid stage times and incorrect balance factors.

Proposed fix
 def layer_partition(self, num_layers: int) -> tuple[int, ...]:
-    return self.partition if self.partition is not None else even_partition(num_layers, self.pp_size)
+    if self.partition is None:
+        return even_partition(num_layers, self.pp_size)
+    if any(layers < 0 for layers in self.partition) or sum(self.partition) != num_layers:
+        raise ValueError(f"partition must contain non-negative entries totaling {num_layers}")
+    return self.partition

As per path instructions, “Verify core SDK API changes remain compatible with … generator inputs.”

📝 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 layer_partition(self, num_layers: int) -> tuple[int, ...]:
return self.partition if self.partition is not None else even_partition(num_layers, self.pp_size)
def layer_partition(self, num_layers: int) -> tuple[int, ...]:
if self.partition is None:
return even_partition(num_layers, self.pp_size)
if any(layers < 0 for layers in self.partition) or sum(self.partition) != num_layers:
raise ValueError(f"partition must contain non-negative entries totaling {num_layers}")
return self.partition
🤖 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 `@aic-core/src/aiconfigurator_core/sdk/pipeline.py` around lines 120 - 121,
Update layer_partition to validate an explicitly configured self.partition
before returning it: reject negative layer counts and partitions whose sum
differs from num_layers, while preserving even_partition(num_layers,
self.pp_size) for unset partitions. Ensure validation remains compatible with
generator-provided partition inputs.

Source: Path instructions

its cost is a large fraction of a single stage's layer work, so it sets the
cycle. Qwen3-32B (64 layers, vocab 151936) on H20 at `pp=8`:

```

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks.

markdownlint reports MD040 for both fences.

Proposed fix
-```
+```text
 stage times (ms): [3.153, 3.150, 3.150, 3.150, 3.150, 3.150, 3.150, 3.901]
@@
-```
+```text
 balance_factor = (step_total / pp_size) / cycle_time

Also applies to: 71-71

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 41-41: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/PIPELINE_PARALLEL_MODELING.md` at line 41, Add the appropriate language
identifier, such as text, to both fenced code blocks in the pipeline parallel
modeling documentation, including the blocks containing stage timing output and
the balance_factor expression, while preserving their contents.

Source: Linters/SAST tools

Comment thread tests/unit/sdk/test_pipeline_modeling.py
…yers

`PipelineSchedule` conflated two things with different consumers:

- **where the work lives** -- layer partition, op placement, per-stage compute
  times, per-hop link cost. Model and hardware knowledge.
- **how the pipe runs** -- microbatch count, P2P overlap, and the closed-form
  `balance_factor` / `fill_factor` that AIC's mean-field step model needs.

Those scalars are surrogates for what an event-driven simulator computes
natively. The Dynamo Mocker embeds the compiled engine for per-iteration cost
estimates and models request-level scheduling itself; if it consumed the
schedule wholesale it would apply AIC's closed-form bubble penalty on top of
its own simulated occupancy and charge for the same bubble twice. It needs the
first half and must not inherit the second.

Split into `PipelineLayout` (shared, no scheduling policy) and
`PipelineSteadyState` (wraps a layout, adds occupancy). The backend hook splits
to match: `_pipeline_layout` for the partition, `_pipeline_steady_state` for the
microbatch policy, the latter composing the former so a backend that overrides
only the partition affects both.

Pure refactor -- no behavior change. Verified bit-identical end to end on H20
(tp8/pp1: TTFT 650.722 / TPOT 18.195 / 207.72 tok/s/gpu; tp1/pp8: 3717.314 /
89.919 / 333.21). Tests restructured along the same boundary, plus one that
pins it: the layout must not expose occupancy-derived attributes.

Doing this before the Mocker integration lands, while the only caller is
`base_backend`, keeps the seam cheap to shape.

Signed-off-by: tianhaox <tianhaox@nvidia.com>

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

🤖 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 `@aic-core/src/aiconfigurator_core/sdk/pipeline.py`:
- Around line 108-124: Add the legacy sdk pipeline compatibility module
corresponding to the core PipelineLayout API, and register "pipeline" in
CORE_SDK_LEAF_MODULES so it is included in cross-package compatibility checks.
Keep the wrapper’s public exports aligned with aiconfigurator_core.sdk.pipeline.

In `@docs/PIPELINE_PARALLEL_MODELING.md`:
- Around line 75-79: Update the two fenced code blocks in the documentation
hunk, including the blocks near stage_times and cycle_time, by adding the text
language identifier to each opening fence. Leave the block contents and closing
fences unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5d1ba94b-ac66-464b-b357-8c0bbcbeea67

📥 Commits

Reviewing files that changed from the base of the PR and between a47383c and ec2387b.

📒 Files selected for processing (4)
  • aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
  • docs/PIPELINE_PARALLEL_MODELING.md
  • tests/unit/sdk/test_pipeline_modeling.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
  • GitHub Check: Collect snapshot (old)
  • GitHub Check: Collect snapshot (new)
  • GitHub Check: Build wheels (manylinux_2_28_x86_64)
  • GitHub Check: Build wheels (manylinux_2_28_aarch64)
  • GitHub Check: Cargo Deny
  • GitHub Check: Build and Test (e2e)
  • GitHub Check: Rust/Python engine-step parity
  • GitHub Check: Build wheels (macosx_arm64)
  • GitHub Check: Build and Test (unit)
  • GitHub Check: aic-core public API contract
🧰 Additional context used
📓 Path-based instructions (4)
**/*

⚙️ CodeRabbit configuration file

**/*: - Prefer applicable inline comments. When the correct fix is clear, small, and limited to the commented diff hunk, include it as a GitHub Suggested Change so the author can apply it with one click.

  • Do not use a suggested change when the fix requires broader design choices, multiple files, generated artifacts, unavailable context, or validation that cannot be inferred from the diff.
  • If a comment is not directly applicable, state the smallest concrete next step and why a one-click suggestion is not safe.

Files:

  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
  • tests/unit/sdk/test_pipeline_modeling.py
  • docs/PIPELINE_PARALLEL_MODELING.md
aic-core/src/aiconfigurator_core/sdk/**

⚙️ CodeRabbit configuration file

aic-core/src/aiconfigurator_core/sdk/**: - Verify core SDK API changes remain compatible with legacy aiconfigurator.sdk imports, generator inputs, profiler data flow, and documented examples.

  • Flag upper-layer dependencies or silent schema drift introduced into the minimal core distribution.

Files:

  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
tests/**

⚙️ CodeRabbit configuration file

tests/**: - Check that tests cover the changed behavior rather than only the happy path.

  • Watch for fixtures or golden outputs that mask backend drift, support-matrix ordering changes, or CLI output regressions.

Files:

  • tests/unit/sdk/test_pipeline_modeling.py
docs/**

⚙️ CodeRabbit configuration file

docs/**: - Check that docs match changed CLI, SDK, generator, backend, and support-matrix behavior.

  • Flag docs that describe unsupported runtimes, stale command names, or behavior not covered by tests or support-matrix evidence.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
🪛 markdownlint-cli2 (0.23.1)
docs/PIPELINE_PARALLEL_MODELING.md

[warning] 75-75: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 86-86: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (2)
docs/PIPELINE_PARALLEL_MODELING.md (1)

65-68: LGTM!

Also applies to: 81-84, 147-153, 197-204

aic-core/src/aiconfigurator_core/sdk/pipeline.py (1)

126-133: Validate explicit partition entries.

layer_partition() accepts negative entries and totals that differ from num_layers. This can create negative stage times or drop or duplicate layer work. Validate the entries before returning the explicit partition. Add tests for negative entries and invalid totals.

Proposed change
 def layer_partition(self, num_layers: int) -> tuple[int, ...]:
-    return self.partition if self.partition is not None else even_partition(num_layers, self.pp_size)
+    if self.partition is None:
+        return even_partition(num_layers, self.pp_size)
+    if any(layers < 0 for layers in self.partition) or sum(self.partition) != num_layers:
+        raise ValueError(f"partition must contain non-negative entries totaling {num_layers}")
+    return self.partition

As per path instructions, “Verify core SDK API changes remain compatible with legacy aiconfigurator.sdk imports, generator inputs, profiler data flow, and documented examples.”

Source: Path instructions

Comment on lines +108 to +124
@dataclass(frozen=True)
class PipelineLayout:
"""Which ops land on which stage, and what each stage costs.

Carries no scheduling policy: it answers "what does stage *i* cost for
this batch" and "what does one hop cost", nothing about occupancy. That
keeps it usable by both AIC's closed-form model and an event-driven
simulator that derives bubbles from occupancy itself.

Args:
pp_size: Number of pipeline stages.
partition: Explicit layers-per-stage. ``None`` uses
:func:`even_partition`.
"""

pp_size: int = 1
partition: tuple[int, ...] | None = None

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline aic-core/src/aiconfigurator_core/sdk/pipeline.py --items all

# Locate legacy and core SDK import paths and public re-exports.
rg -n -C3 --glob '*.py' \
  'from aiconfigurator(_core)?\.sdk|import aiconfigurator(_core)?\.sdk|PipelineLayout|PipelineSteadyState' .

# Trace configuration and profiler inputs that can construct or influence layouts.
rg -n -C3 --glob '*.py' \
  'pipeline_microbatches|pp_size|partition|layer_partition|per_op_ms' aic-core tests

# Check documented examples for obsolete imports or incompatible PP claims.
rg -n -C3 \
  'aiconfigurator\.sdk|PipelineLayout|PipelineSteadyState|pipeline_microbatches|pp_size' docs README.md 2>/dev/null || true

Repository: ai-dynamo/aiconfigurator

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- pipeline module outline ---'
ast-grep outline aic-core/src/aiconfigurator_core/sdk/pipeline.py --items all

echo '--- pipeline module ---'
sed -n '1,280p' aic-core/src/aiconfigurator_core/sdk/pipeline.py

echo '--- exact layout and pipeline symbols ---'
rg -n -C4 --glob '*.py' \
  '\b(PipelineLayout|PipelineSteadyState|even_partition|pipeline_microbatches|per_op_ms)\b' \
  aic-core/src aic-core/tests tests 2>/dev/null | head -n 1200

echo '--- legacy package and compatibility files ---'
git ls-files | rg '(^|/)(aiconfigurator|aic-core)/.*(sdk|compat|init).*\.py$' | head -n 400

echo '--- legacy-to-core imports and re-exports ---'
rg -n -C3 --glob '*.py' \
  'aiconfigurator_core|aiconfigurator\.sdk|__all__|import_module|sys\.modules' \
  aiconfigurator aic-core/src 2>/dev/null | head -n 1600

echo '--- docs and examples ---'
rg -n -C3 \
  'aiconfigurator\.sdk|aiconfigurator_core\.sdk|PipelineLayout|PipelineSteadyState|pipeline_microbatches|per_op_ms|partition' \
  docs README.md examples 2>/dev/null | head -n 1000

Repository: ai-dynamo/aiconfigurator

Length of output: 46207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- all tracked files containing pipeline API names ---'
rg -l --glob '*.py' \
  '\b(PipelineLayout|PipelineSteadyState|even_partition|pipeline_microbatches|per_op_ms)\b' . \
  | sort

echo '--- configuration, generator, and profiler definitions/usages ---'
rg -n -C5 --glob '*.py' \
  '(class .*Config|def .*generate|def .*profile|profiler|generator|pipeline|pp_size|partition)' \
  aic-core/src/aiconfigurator_core/sdk aiconfigurator/sdk tests/cross_package tests/unit/sdk 2>/dev/null \
  | rg 'PipelineLayout|PipelineSteadyState|even_partition|pipeline_microbatches|per_op_ms|partition|pp_size|profiler|generator|Config' \
  | head -n 1800

echo '--- package init/export surfaces ---'
for f in $(git ls-files '*__init__.py' | rg '(^|/)(aiconfigurator|aic-core).*(sdk|__init__)' | head -n 200); do
  matches=$(rg -n 'pipeline|Pipeline|aiconfigurator_core' "$f" || true)
  if [ -n "$matches" ]; then
    echo "### $f"
    printf '%s\n' "$matches"
  fi
done

echo '--- docs/readmes mentioning pipeline API ---'
rg -n -C4 \
  'PipelineLayout|PipelineSteadyState|even_partition|pipeline_microbatches|per_op_ms|partition' \
  docs README.md aic-core/src/aiconfigurator_core/sdk/models/README.md 2>/dev/null | head -n 1000

Repository: ai-dynamo/aiconfigurator

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package layout ---'
git ls-files | rg '(^|/)(aiconfigurator|aiconfigurator_core)(/|$)' | head -n 500

echo '--- public package initializers ---'
for f in $(git ls-files | rg '(^|/)(__init__\.py)$' | rg 'aiconfigurator'); do
  echo "### $f"
  cat -n "$f" | head -n 220
done

echo '--- legacy SDK pipeline-related references only ---'
rg -n -C5 --glob '*.py' \
  'pipeline|pp_size|per_op_ms|generation_ops|context_ops|profiler|profile|generator' \
  aiconfigurator aic-core/src/aiconfigurator 2>/dev/null \
  | rg 'pipeline|pp_size|per_op_ms|Pipeline|profile|generator|generation_ops|context_ops' \
  | head -n 1800

echo '--- backend integration around pipeline calls ---'
sed -n '90,150p' aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
sed -n '1500,1585p' aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
sed -n '1420,1465p' aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py

echo '--- setup/package mapping ---'
rg -n -C5 --glob 'pyproject.toml' --glob 'setup.py' --glob 'setup.cfg' --glob '*.toml' \
  'aiconfigurator|aic-core|package|packages|src' . | head -n 1200

Repository: ai-dynamo/aiconfigurator

Length of output: 46792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- every pipeline method call and definition ---'
rg -n -C6 --glob '*.py' \
  '(_pipeline_layout|_pipeline_steady_state|pipeline_microbatches|PipelineLayout|PipelineSteadyState)' \
  aic-core/src tests | head -n 2200

echo '--- public estimate/generator signatures and forwarding ---'
rg -n -C8 --glob '*.py' \
  'def (estimate|generate|compile|profile|query|run)|\*\*kwargs|pipeline_microbatches' \
  aic-core/src/aiconfigurator_core/sdk aic-core/src/aiconfigurator 2>/dev/null \
  | head -n 2400

echo '--- model operation construction and profiler breakdown shapes ---'
rg -n -C5 --glob '*.py' \
  '(context_ops|generation_ops|per_op|breakdown|latency_breakdown|query\(|profiler)' \
  aic-core/src/aiconfigurator_core/sdk/models \
  aic-core/src/aiconfigurator_core/sdk/backends \
  aic-core/src/aiconfigurator_core/sdk/perf_interp \
  | head -n 2600

echo '--- documentation PP references ---'
rg -n -C5 \
  'PP|pp_size|pipeline|pipeline parallel|chunked prefill|microbatch' \
  docs README.md aic-core/src/aiconfigurator_core/sdk/models/README.md 2>/dev/null \
  | head -n 1800

Repository: ai-dynamo/aiconfigurator

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- project metadata and package mappings ---'
for f in $(git ls-files | rg '(^|/)(pyproject\.toml|setup\.py|setup\.cfg|tox\.ini|pytest\.ini)$'); do
  echo "### $f"
  rg -n -C8 'aiconfigurator|aic-core|package|packages|src|build-system|dependencies' "$f" || true
done

echo '--- exact pipeline call sites ---'
rg -n -C12 --glob '*.py' \
  '(_pipeline_layout|_pipeline_steady_state|pipeline_microbatches|PipelineLayout|PipelineSteadyState)' \
  aic-core/src tests/unit/sdk tests/cross_package \
  | head -n 1600

echo '--- relevant backend signatures and forwarding ---'
sed -n '1360,1480p' aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py
sed -n '1500,1580p' aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py

echo '--- docs and examples with PP or pipeline terms ---'
rg -n -C4 \
  'PP|pp_size|pipeline|pipeline parallel|chunked prefill|microbatch' \
  docs README.md aic-core/src/aiconfigurator_core/sdk/models/README.md 2>/dev/null \
  | head -n 1600

Repository: ai-dynamo/aiconfigurator

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

roots = [Path("aic-core/src"), Path("tests"), Path("docs")]
hits = []

for root in roots:
    if not root.exists():
        continue
    for path in root.rglob("*.py"):
        try:
            tree = ast.parse(path.read_text(), filename=str(path))
        except (OSError, SyntaxError):
            continue
        for node in ast.walk(tree):
            if isinstance(node, ast.ImportFrom):
                module = node.module or ""
                if "aiconfigurator" in module or "pipeline" in module:
                    names = [a.name for a in node.names]
                    hits.append((str(path), node.lineno, f"from {module} import {', '.join(names)}"))
            elif isinstance(node, ast.Import):
                for alias in node.names:
                    if "aiconfigurator" in alias.name:
                        hits.append((str(path), node.lineno, f"import {alias.name}"))

print("--- parsed import references ---")
for item in sorted(hits):
    print(f"{item[0]}:{item[1]}: {item[2]}")

print("--- pipeline API definitions and call keywords ---")
for path in [Path("aic-core/src/aiconfigurator_core/sdk/backends/base_backend.py")]:
    tree = ast.parse(path.read_text(), filename=str(path))
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if node.name in {"_pipeline_layout", "_pipeline_steady_state"}:
                args = [a.arg for a in node.args.args + node.args.kwonlyargs]
                print(f"{path}:{node.lineno}: {node.name}({', '.join(args)})")
            for call in ast.walk(node):
                if isinstance(call, ast.Call):
                    text = ast.unparse(call.func)
                    if "_pipeline_" in text or any(
                        isinstance(k, ast.keyword) and k.arg == "pipeline_microbatches"
                        for k in call.keywords
                    ):
                        print(f"{path}:{call.lineno}: {ast.unparse(call)}")
PY

Repository: ai-dynamo/aiconfigurator

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- legacy SDK files ---'
git ls-files 'src/aiconfigurator/sdk/**' 'src/aiconfigurator/**' | head -n 500

echo '--- legacy SDK package initializers ---'
for f in $(git ls-files 'src/aiconfigurator/**/__init__.py' 'src/aiconfigurator/__init__.py'); do
  echo "### $f"
  cat -n "$f" | head -n 260
done

echo '--- legacy SDK modules that import or alias core modules ---'
rg -n -C5 --glob '*.py' \
  'aiconfigurator_core|from \.|\.\.sdk|ModelConfig|RuntimeConfig|pipeline|pp_size|per_op_ms|generation_ops|context_ops' \
  src/aiconfigurator/sdk src/aiconfigurator/generator 2>/dev/null \
  | head -n 2400

echo '--- compatibility tests ---'
sed -n '1,220p' tests/cross_package/test_import_contract.py
rg -n -C8 --glob '*.py' \
  'aiconfigurator\.sdk|aiconfigurator_core\.sdk|pipeline_microbatches|pp_size|partition|profiler|generator' \
  tests/cross_package tests/unit tests/integration tests/e2e tests/golden \
  | head -n 2200

Repository: ai-dynamo/aiconfigurator

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- pipeline compatibility files ---'
for f in \
  src/aiconfigurator/sdk/pipeline.py \
  src/aiconfigurator/sdk/_compat.py \
  src/aiconfigurator/sdk/backends/base_backend.py \
  aic-core/src/aiconfigurator_core/sdk/__init__.py \
  tests/cross_package/test_import_contract.py; do
  if [ -f "$f" ]; then
    echo "### $f"
    cat -n "$f" | sed -n '1,240p'
  else
    echo "MISSING: $f"
  fi
done

echo '--- exact pipeline paths and references ---'
git ls-files | rg '(^|/)pipeline\.py$|pipeline_microbatches|CORE_SDK_LEAF_MODULES' || true

python3 - <<'PY'
from pathlib import Path
import ast

core = Path("aic-core/src/aiconfigurator_core/sdk")
legacy = Path("src/aiconfigurator/sdk")
contract = Path("tests/cross_package/test_import_contract.py")

core_leaves = sorted(
    str(p.relative_to(core)).removesuffix(".py").replace("/", ".")
    for p in core.rglob("*.py")
    if p.name != "__init__.py"
)
legacy_leaves = sorted(
    str(p.relative_to(legacy)).removesuffix(".py").replace("/", ".")
    for p in legacy.rglob("*.py")
    if p.name != "__init__.py"
)
tree = ast.parse(contract.read_text())
declared = []
for node in tree.body:
    if isinstance(node, ast.Assign):
        for target in node.targets:
            if isinstance(target, ast.Name) and target.id == "CORE_SDK_LEAF_MODULES":
                declared = sorted(ast.literal_eval(node.value))
print("--- leaf-set comparison ---")
print("core-only:", sorted(set(core_leaves) - set(legacy_leaves)))
print("legacy-only:", sorted(set(legacy_leaves) - set(core_leaves)))
print("declared-missing:", sorted(set(core_leaves) - set(declared)))
print("declared-extra:", sorted(set(declared) - set(core_leaves)))
PY

Repository: ai-dynamo/aiconfigurator

Length of output: 13821


Add the legacy pipeline compatibility wrapper. Add src/aiconfigurator/sdk/pipeline.py and include "pipeline" in CORE_SDK_LEAF_MODULES; the cross-package contract currently fails because aiconfigurator_core.sdk.pipeline has no legacy counterpart.

🤖 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 `@aic-core/src/aiconfigurator_core/sdk/pipeline.py` around lines 108 - 124, Add
the legacy sdk pipeline compatibility module corresponding to the core
PipelineLayout API, and register "pipeline" in CORE_SDK_LEAF_MODULES so it is
included in cross-package compatibility checks. Keep the wrapper’s public
exports aligned with aiconfigurator_core.sdk.pipeline.

Source: Path instructions

Comment on lines +75 to +79
```
stage_times(per_op_ms, num_layers) -> [t_0, t_1, ... t_{pp-1}]
per_hop_latency(per_op_ms) -> P2P cost of one stage-to-stage hop
layer_partition(num_layers) -> layers per stage
```

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to the new fenced code blocks.

The fences at Line 75 and Line 86 have no language identifier and trigger MD040. Add text to both openings.

This is the same unresolved issue noted in the previous review.

As per path instructions, the fix is small and limited to this documentation hunk, so a one-click suggested change is safe.

Proposed fix
-```
+```text
 stage_times(per_op_ms, num_layers) -> [t_0, t_1, ... t_{pp-1}]
 per_hop_latency(per_op_ms)         -> P2P cost of one stage-to-stage hop
 layer_partition(num_layers)        -> layers per stage
-```
+```
@@
-```
+```text
 cycle_time     = max_i(stage_time_i) + p2p_per_hop
 balance_factor = (step_total / pp_size) / cycle_time      # per-microbatch latency effect
 fill_factor    = min(1, num_microbatches / pp_size)       # throughput-only effect
 efficiency     = balance_factor * fill_factor
-```
+```

Also applies to: 86-91

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 75-75: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/PIPELINE_PARALLEL_MODELING.md` around lines 75 - 79, Update the two
fenced code blocks in the documentation hunk, including the blocks near
stage_times and cycle_time, by adding the text language identifier to each
opening fence. Leave the block contents and closing fences unchanged.

Sources: Path instructions, Linters/SAST tools

…dder path

The previous framing justified the layout/steady-state split with a scenario
that is not the current contract, and understated the scope limit. Both are
corrected here; no code behavior changes.

What the embedder contract actually is:
`ForwardPassPerfModel::estimate_forward_pass_time_ms` takes per-ATTENTION-DP-
rank `ForwardPassMetrics` and reduces with `max` over ranks. There is no stage
axis in it. A PP worker reports one forward pass and the caller expects the
estimate to already include intra-worker parallelism, so PP is AIC's job to
model, not the caller's. That also makes the placement obvious by symmetry:
`forward_pass_time_ms` maxes over DP ranks, a pipeline cycle maxes over
stages -- same reduction, different axis.

So the split is justified by keeping the placement/partition RULES in one
place (so a second derivation cannot drift from `stage_times`), not by a
double-counting risk in a simulator that models occupancy itself. Rewrote the
module docstring, the design doc, and the boundary test's rationale.

Two gaps documented that the earlier text missed:

1. This change covers `run_agg` only. The planner/Mocker path
   (`ForwardPassPerfModel` -> `forward_pass_time_ms` -> `rank_latency_ms`)
   never enters `run_agg` and is still ideal-PP. Fixing it means the same
   max-over-stages inside `rank_latency_ms` -- a Rust change against a literal
   port, so its own PR under the rust-core parity rule.

2. The FPM online correction silently absorbs the PP error. `tune_with_fpms`
   learns `median(observed_ms / native_ms)` where `native_ms` is the
   whole-model estimate; the only guard is finite-and-positive, so the factor
   is UNBOUNDED in magnitude, and the region key is built from scheduler counts
   only, so `pp_size` is not a feature. Depending on what the emitter reports
   as `wall_time`, either PP's throughput benefit is absent (whole-iteration
   wall time) or the correction learns ~1/pp_size and stands in for a
   structural effect (per-stage wall time). Either way it is laundered through
   a fitted factor with no warning.

Replaces the earlier "stage-indexed cost oracle / FFI is future work" note,
which was written on the wrong premise.

Signed-off-by: tianhaox <tianhaox@nvidia.com>

@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 `@docs/PIPELINE_PARALLEL_MODELING.md`:
- Line 101: Update the documentation phrase in the steady-state collapse
discussion from “one step shape” to the hyphenated “one-step shape,” without
changing surrounding text.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8f2faf40-04d9-49d4-90fd-5a4aec7698e2

📥 Commits

Reviewing files that changed from the base of the PR and between ec2387b and 5c72500.

📒 Files selected for processing (3)
  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
  • docs/PIPELINE_PARALLEL_MODELING.md
  • tests/unit/sdk/test_pipeline_modeling.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/sdk/test_pipeline_modeling.py
  • aic-core/src/aiconfigurator_core/sdk/pipeline.py
📜 Review details
⏰ Context from checks skipped due to timeout. (10)
  • GitHub Check: Collect snapshot (new)
  • GitHub Check: Collect snapshot (old)
  • GitHub Check: Build wheels (macosx_arm64)
  • GitHub Check: Build wheels (manylinux_2_28_x86_64)
  • GitHub Check: Build wheels (manylinux_2_28_aarch64)
  • GitHub Check: aic-core public API contract
  • GitHub Check: Cargo Deny
  • GitHub Check: Build and Test (e2e)
  • GitHub Check: Build and Test (unit)
  • GitHub Check: Rust/Python engine-step parity
🧰 Additional context used
📓 Path-based instructions (2)
**/*

⚙️ CodeRabbit configuration file

**/*: - Prefer applicable inline comments. When the correct fix is clear, small, and limited to the commented diff hunk, include it as a GitHub Suggested Change so the author can apply it with one click.

  • Do not use a suggested change when the fix requires broader design choices, multiple files, generated artifacts, unavailable context, or validation that cannot be inferred from the diff.
  • If a comment is not directly applicable, state the smallest concrete next step and why a one-click suggestion is not safe.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
docs/**

⚙️ CodeRabbit configuration file

docs/**: - Check that docs match changed CLI, SDK, generator, backend, and support-matrix behavior.

  • Flag docs that describe unsupported runtimes, stale command names, or behavior not covered by tests or support-matrix evidence.

Files:

  • docs/PIPELINE_PARALLEL_MODELING.md
🪛 LanguageTool
docs/PIPELINE_PARALLEL_MODELING.md

[grammar] ~101-~101: Use a hyphen to join words.
Context: ...e*, valid only because AIC evaluates one step shape and assumes every stage sees ...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.23.1)
docs/PIPELINE_PARALLEL_MODELING.md

[warning] 115-115: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (2)
docs/PIPELINE_PARALLEL_MODELING.md (2)

115-121: 📐 Maintainability & Code Quality

Add a language identifier to this fenced block.

MD040 flags the fence at Line 115. The block contains pseudo-code, so use text and preserve its contents.

As per path instructions, this fix is small and limited to this documentation hunk, so a one-click suggested change is safe.

Suggested change
-```
+```text

Sources: Path instructions, Linters/SAST tools


95-100: LGTM!

Also applies to: 102-114, 116-121, 161-163, 207-215, 217-236

is what stops two derivations from drifting.

`balance_factor` and `fill_factor` are a different kind of thing: a
steady-state *collapse*, valid only because AIC evaluates one step shape and

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a hyphen in “one-step shape.”

Change one step shape to one-step shape.

Suggested change
- steady-state *collapse*, valid only because AIC evaluates one step shape and
+ steady-state *collapse*, valid only because AIC evaluates one-step shape and

As per path instructions, this fix is small and limited to this documentation hunk, so a one-click suggested change is safe.

📝 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
steady-state *collapse*, valid only because AIC evaluates one step shape and
steady-state *collapse*, valid only because AIC evaluates one-step shape and
🧰 Tools
🪛 LanguageTool

[grammar] ~101-~101: Use a hyphen to join words.
Context: ...e*, valid only because AIC evaluates one step shape and assumes every stage sees ...

(QB_NEW_EN_HYPHEN)

🤖 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 `@docs/PIPELINE_PARALLEL_MODELING.md` at line 101, Update the documentation
phrase in the steady-state collapse discussion from “one step shape” to the
hyphenated “one-step shape,” without changing surrounding text.

Sources: Path instructions, Linters/SAST tools

Qwen3-32B bf16 dummy weights, 8x H20-3e, TRT-LLM 1.3.0rc20 against the
h20e_sxm perf DB collected on the same stack and version, so absolute numbers
are comparable. Decode-dominated (isl=256, osl=512), 8 GPUs held constant
across every (tp, pp), chunked prefill off, aiperf closed-loop.

Three findings, documented in a new section 6.

1. PP is a pure loss at constant GPU count for this dense model: no (tp, pp)
   beats tp8/pp1 at any concurrency (best 0.62x, pp=8 lands at 0.28-0.40x).
   Expected for weight-bound decode -- tp*pp fixed means per-GPU weight bytes
   are unchanged, so pipelining buys nothing while P2P hops and the smaller
   per-stage batch cost real time. Evidence for leaving PP out of the automatic
   search for now.

2. The "always full pipe" default is the largest single error. At most
   min(pp, C) microbatches can be in flight; the default assumes pp. At C=1
   this over-predicts by up to 645% (pp=8: 357.6 vs 48.0 measured). Driving
   num_microbatches from real concurrency cuts that to -7%. The fill_factor
   mechanism added here is the right shape -- but nothing sets it, because
   run_agg derives concurrency as batch_size * pp_size rather than taking it as
   a target. Recorded as gap 1.

3. A concurrency-dependent gap remains unexplained. For C >= pp the fill
   correction is inert and AIC still over-predicts, growing from +7..44% at
   C=8 to +73..173% at C=128. Stage imbalance moves this by ~1 point, so it is
   not the cause. Measured behaviour is non-monotonic in pp (at C=128, pp=2 ITL
   49.8ms vs pp=4 32.8ms) and pp=2 throughput falls between C=64 and C=128
   while ITL doubles -- a saturation signature pointing at engine-side
   scheduling. Flagged explicitly as not-to-be-fitted until understood.

The pp=1 baseline predicts within -8%..+11% throughput and 8% TPOT throughout,
so all of the above is PP-specific rather than a perf-database problem.

This reframes the change: the stage-imbalance model in commit 1 is correct but
second-order against silicon. The dominant errors are the fill default and the
unexplained saturation. Gap 1 is updated accordingly.

Signed-off-by: tianhaox <tianhaox@nvidia.com>
…oss-node

The previous revision drew a conclusion the experiment could not support. The
silicon run is single-node, and PP cannot win inside one NVLink domain at
constant GPU count: per-GPU weight bytes are unchanged (tp*pp fixed), so
pipelining buys nothing while the P2P hops and the smaller per-stage batch cost
real time. Reporting that as "PP is a pure loss", and using it to argue PP
belongs out of the search, generalised from the one regime where PP is
structurally incapable of paying off.

PP exists for the case where TP would otherwise span the slow fabric. On this
spec (num_gpus_per_node=8, inter_node 50 GB/s vs intra_node 450 GB/s) TP
all-reduces across that 9x cliff every layer while PP crosses it once per stage
boundary. AIC models this, and the effect is large -- Qwen3-32B, isl=256/
osl=512, C=64:

    16 GPUs (2 nodes):  tp16/pp1 1714 tok/s  vs  tp8/pp2 7073  -> 4.1x
    32 GPUs (4 nodes):  tp32/pp1 1735 tok/s  vs  tp8/pp4 9275  -> 5.4x

So section 6.2 (fill default) and 6.3 (saturation gap) are restated as error-
mode stress tests: single-node PP exposes the model's failure modes precisely
because no real gain can hide them. Which findings generalise is now stated --
the fill default and stage imbalance are topology-independent, while the
saturation effect may be TRT-LLM single-node scheduling and is untested
cross-node.

Also corrects the search-space gap. Leaving PP out is not a conservative
default: at 16 GPUs the search can only offer tp16/pp1 when tp8/pp2 is worth
4x, which is a wrong answer on a real sizing decision, not a cautious one. It
stays gated on the gaps only because both over-predict, so enabling the
dimension now would select pipelines that are too deep.

Signed-off-by: tianhaox <tianhaox@nvidia.com>
@natoscott

natoscott commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Multi-backend PP validation data — 5 models, 3 backends, 8xH200

I recently ran an independent PP validation campaign that may be useful context for this PR. The data covers the regime the silicon validation in §6 of docs/PIPELINE_PARALLEL_MODELING.md targets (single-node, 8 GPUs held constant across TP/PP splits) but extends it across backends and model sizes.

Methodology

  • Hardware: 8x H200 SXM (single node, NVLink domain)
  • Backends: vLLM 0.9.1, SGLang v0.5.13.post1, TRT-LLM 1.3.0rc18 (pytorch backend)
  • Models: Qwen3-0.6B, 8B, 14B, 32B-FP8, Llama-3.1-70B-FP8 (TRT-LLM incompatible with this model's FP8 quantization)
  • PP configs: TP=8/PP=1, TP=4/PP=2, TP=2/PP=4, TP=1/PP=8
  • Workload: ISL=4000, OSL=1000, max_duration=120s, guidellm concurrent mode
  • Metric: Peak output tok/s/gpu across concurrency sweep (1, 2, 4, 8, 16, 32), normalized to TP=8/PP=1 baseline

All three backends target the same models on the same hardware with the same workload, so throughput ratios are directly comparable.

Observed PP efficiency

Config vLLM SGLang TRT-LLM
Qwen3-0.6B
TP=4/PP=2 0.791 0.991 0.775
TP=2/PP=4 0.563 0.559 0.135
TP=1/PP=8 0.349 0.307 0.106
Qwen3-8B
TP=4/PP=2 0.798 0.815 0.759
TP=2/PP=4 0.559 0.569 0.193
TP=1/PP=8 0.315 0.315 0.106
Qwen3-14B
TP=4/PP=2 0.763 0.807 0.689
TP=2/PP=4 0.517 0.492 0.296
TP=1/PP=8 0.308 0.264 0.156
Qwen3-32B-FP8
TP=4/PP=2 0.783 0.761 0.679
TP=2/PP=4 0.525 0.410 0.564
TP=1/PP=8 0.308 0.231 0.185
Llama-70B-FP8
TP=4/PP=2 0.789 0.662 --
TP=2/PP=4 0.456 0.542 --
TP=1/PP=8 0.221 0.163 --

How much of the PP penalty does balance_factor explain?

I decomposed the observed efficiency into the balance_factor component (stage imbalance from lm_head placement) and a scheduling residual. Using a rough decode-step cost model (per-layer ~ 4h^2 + 3h*intermediate, lm_head ~ h*vocab):

Model PP Total penalty balance_factor explains Scheduling residual
Qwen3-8B 2 20.2% 14.7% (73%) 5.5%
Qwen3-8B 8 68.5% 40.7% (59%) 27.8%
Qwen3-14B 8 69.2% 33.1% (48%) 36.1%
Qwen3-32B-FP8 8 69.2% 23.6% (34%) 45.6%
Llama-70B-FP8 8 77.9% 11.5% (15%) 66.4%

The balance_factor accounts for 15-73% of the total PP penalty, with the fraction decreasing for larger models (where per-layer compute dominates lm_head cost and the stages are more balanced). The majority of the penalty — especially for larger models — comes from the scheduling residual flagged in §6.3 of docs/PIPELINE_PARALLEL_MODELING.md.

The scheduling residual has structure

The residual (observed_eff / balance_factor) fits r = 1 - s * log2(pp) reasonably well for vLLM and SGLang. The coefficient s represents the scheduling/synchronization overhead component:

Model vLLM s SGLang s TRT-LLM s
Qwen3-8B 0.158 0.150 0.283
Qwen3-14B 0.195 0.192 0.288
Qwen3-32B-FP8 0.212 0.250 0.256
Llama-70B-FP8 0.239 0.274 --

Two patterns:

  1. s increases with model size (0.15 -> 0.24 for vLLM). This is counter-intuitive if the overhead were a fixed per-hop ms cost — it suggests the scheduling overhead scales with something beyond simple synchronization (memory bandwidth contention, KV cache management, activation tensor size).
  2. vLLM and SGLang s values are close (within 0.03 for 8B-14B), diverging for 32B+ models. TRT-LLM s is consistently higher (~0.27).

Combined model accuracy

I compared three modeling approaches for vLLM (models 8B-70B, PP=2/4/8):

Approach Mean abs error Max abs error
Flat empirical coefficient (c=0.228) 2.7 pp 9.5 pp
balance_factor * (1 - s_per_model * log2(pp)) 1.8 pp 4.7 pp
balance_factor * (1 - s_mean * log2(pp)) 4.6 pp 14.2 pp

The combined model with per-model s is the most accurate, halving max error. But using a single mean s per backend is actually worse than the flat coefficient — s varies enough with model size that collapsing it into one number introduces more error than the balance_factor removes.

For SGLang: flat c gives 5.2/11.7 pp mean/max error; combined per-model gives 3.7/10.7 pp.

For TRT-LLM: all approaches struggle (6-8 pp mean, 16-21 pp max) due to the non-monotonic PP=4 behavior described below.

TRT-LLM PP=4 cliff

TRT-LLM shows a non-monotonic PP efficiency pattern across all model sizes tested:

Model PP=2 c PP=4 c PP=8 c
Qwen3-0.6B 0.225 0.432 0.298
Qwen3-8B 0.241 0.404 0.298
Qwen3-14B 0.311 0.352 0.281
Qwen3-32B-FP8 0.321 0.218 0.272

(Per-PP-level coefficient c from eff = 1 - c * log2(pp))

For small/medium models (0.6B-14B), the PP=4 per-level coefficient spikes to 0.35-0.43 — meaning PP=4 is disproportionately worse than either PP=2 or PP=8 would predict from a smooth curve. The per-concurrency ITL data confirms this is consistent across all concurrency levels (not a measurement artifact):

TRT-LLM Qwen3-8B conc=1 ITL:
  TP=8/PP=1:  2.9 ms
  TP=4/PP=2:  5.7 ms  (2.0x)
  TP=2/PP=4: 17.1 ms  (5.9x)  <-- step function
  TP=1/PP=8: 18.7 ms  (6.4x)

This pattern is absent from vLLM and SGLang (smooth degradation) and may relate to TRT-LLM's MPI-based pipeline implementation handling the TP=2 per-stage configuration differently. The effect diminishes for 32B-FP8 where per-stage compute is larger relative to the overhead.

This appears related to the non-monotonic saturation behavior identified in §6.3 of docs/PIPELINE_PARALLEL_MODELING.md — the scheduling overhead may depend on the interaction between TP and PP configuration within each stage, not just PP alone.

Implications

The balance_factor is structurally correct — it captures the right mechanism (lm_head imbalance). The data shows:

  1. The scheduling residual is the dominant component for models >= 14B (52-85% of total penalty). Any calibration effort should prioritize this over refining balance further.
  2. The residual is backend-specific. vLLM and SGLang share similar PP scheduling behavior; TRT-LLM is materially different. This has implications for whether the correction belongs in BaseBackend vs per-backend overrides.
  3. The residual scales with model size in a way a single per-backend constant doesn't capture. The fill_factor hook may be the right place to absorb this, but the relationship needs characterization.
  4. TRT-LLM's PP=4 cliff suggests the overhead depends on the TP*PP interaction, not just PP alone.

The raw benchmark data (224 result files across all backends) and analysis scripts are available if useful for calibration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants