Skip to content

feat(dataset): typed distribution sampling, percentile targets, sticky sequence buckets, first-turn context#1130

Open
ajcasagrande wants to merge 14 commits into
mainfrom
ajc/yaml-dist-wiring
Open

feat(dataset): typed distribution sampling, percentile targets, sticky sequence buckets, first-turn context#1130
ajcasagrande wants to merge 14 commits into
mainfrom
ajc/yaml-dist-wiring

Conversation

@ajcasagrande

@ajcasagrande ajcasagrande commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes every typed SamplingDistribution in YAML configs actually sample at runtime, adds a percentile-targeting distribution, and introduces conversation-class workload modeling: sticky ISL/OSL buckets with per-bucket first-turn seed context, plus distribution-valued prefix context sizes.

Distribution wiring (first 8 commits)

  • Fix flattening bug: lognormal/multimodal/empirical ISL/OSL configs silently collapsed to a constant at the distribution mean. All shapes now sample their full typed distribution per turn (no-double-sampling: PromptGenerator.generate receives stddev=0 so variance comes from the per-turn draw only).
  • New PercentileDistribution{p50, p99[, mean]}: lognormal fit for two targets, deterministic mixture solver when mean is also pinned. Solved eagerly at config-validation time; infeasible targets fail in aiperf config validate, not mid-benchmark.
  • prompts.first_turn_isl — starting-context distribution for growing multi-turn conversations: turn 1 samples it, turns 2+ sample isl.
  • Relative bucket weightssequence_distribution probabilities no longer must sum to 100; probability: 0 disables a bucket. Same relaxation in the legacy --seq-dist string parser.
  • Typed distributions wired for turns, turn_delay, image width/height, audio length, and rankings shapes.

Conversation classes (last 4 commits)

  • Sticky buckets: sequence_distribution now draws ONE bucket per conversation and keeps it for every turn, so ISL/OSL stay tied for the conversation's life (e.g. huge-context conversations keep tiny outputs). Single-turn workloads are byte-identical to the previous per-turn draw (RNG-stream replay test enforces this).
  • Per-bucket first_turn_isl: each bucket sizes its own seed context (turn 1) separately from per-turn growth (isl). Top-level first_turn_isl + sequence_distribution is now a config error pointing at the per-bucket field.
  • Prefix context distributions: prefix_prompts.shared_system_length / user_context_length accept any distribution — shared system sampled once per run (prompt identical across sessions, preserving its KV-cache purpose), user context sampled once per session.
prompts:
  sequence_distribution:
    - first_turn_isl: {p50: 20000, p99: 100000, mean: 30000}  # seed context
      isl: {mean: 300, stddev: 100}                            # per-turn growth
      osl: {mean: 250, stddev: 80}
      probability: 50
    - {first_turn_isl: {mean: 400000, stddev: 20000}, isl: {mean: 500, stddev: 200}, osl: 64, probability: 1}
prefix_prompts:
  shared_system_length: {mean: 2048, stddev: 512, min: 512}
  user_context_length: {p50: 4000, p99: 32000, mean: 6000}

Behavior change

Multi-turn conversations with sequence_distribution previously drew a fresh bucket per turn; they now hold one bucket per conversation. Documented with a callout in docs/tutorials/sequence-distributions.md. Single-turn behavior is unchanged (test-enforced byte-compat).

Testing

  • Full unit suite green at HEAD: 13,911 passed, 94 skipped.
  • New coverage: sticky-bucket invariants (disjoint-range classes never mix), first-turn routing + fallback, single-turn RNG byte-compat replay oracle, percentile E2E (p50/p99/mean targets hit through the real composer), prefix-length per-run/per-session sampling cardinality, validator conflict/feasibility errors.
  • Docs: docs/tutorials/yaml-distributions.md (new) + sequence-distributions.md updated; JSON schema and generated artifacts regenerated.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added percentile-based token-length distributions (p50/p99 with optional mean) including min/max clamping.
    • Added first-turn-only sequence length controls (first_turn_isl, including per-bucket overrides) for multi-turn runs.
    • Extended prefix/shared context length fields to accept sampling distributions (sampled per run/session as appropriate).
  • Documentation

    • Updated CLI and YAML guidance for sequence-distribution probabilities/weights to clarify relative-weight semantics and normalization.
  • Bug Fixes

    • Sequence-distribution “probabilities” are now non-negative relative weights (normalized at sampling time); buckets with zero weight are disabled.
    • Sticky bucket selection is applied consistently across all turns in a conversation.
    • User max_turns is clamped to the sampled conversation length.

ajcasagrande and others added 12 commits July 7, 2026 14:02
Add a sixth sampling distribution parameterized by percentile targets:
{p50, p99} fits a log-normal exactly; {p50, p99, mean} solves a
two-component truncated-normal mixture deterministically at validation
time (stdlib math only, no scipy). Register in the discriminated union
via a p50/p99 discriminator rule ahead of the median check. Fix the base
Distribution.__getattr__ to delegate PrivateAttr reads to Pydantic so the
solved-parameters cache resolves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
…ning, no double-sampling)

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
Replace the mean+stddev flattening in _build_sequence_distribution with a
private _TypedSequenceDistribution runtime class: pick a bucket by weight,
then draw entry.isl.sample_int(rng) / entry.osl.sample_int(rng) so
lognormal/multimodal/empirical/percentile buckets keep their full shape.

Also fix SyntheticDatasetComposer._include_prompt to enable text when a
sequence_distribution is configured (native YAML seq-dist without isl
previously raised 'All synthetic data are disabled'; the CLI path masked
this by defaulting isl.mean=550).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
…dio, rankings

Rewire the remaining SamplingDistribution consumers to draw from their full
typed shape (lognormal/multimodal/empirical/percentile) instead of flattening
to (expected_value, stddev-if-Normal):

- synthetic.py: turns via sample_int (floors at 1); turn_delay via sample()
  gated on expected_value > 0 (preserves mean-0 disables delay), now float.
- image.py: width/height via sample_int.
- audio.py: length via sample() clamped to max(0.01, ...); keeps < 0.01 config
  error on expected_value.
- synthetic_rankings.py: passages/query_tokens/passage_tokens via sample_int,
  removing the calculate_num_tokens double-sample.

Delete now-dead _stddev_int (synthetic) and _stddev (rankings) helpers; keep
_expected for the config-time _include_* gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
…ights

Add PromptConfig.first_turn_isl to size the first turn's starting context
in growing multi-turn benchmarks while isl sizes each subsequent turn's
new input; wired through _get_turn_sequence_lengths(is_first=...).

Relax ISL/OSL sequence-distribution bucket weights to positive relative
weights (normalized at sampling time); they no longer must sum to 100.
Applies to both the typed sequence_distribution path and the legacy CLI
string parser. Non-positive totals are still rejected to guard the
normalization divide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
…n buckets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
…t buckets, get_statistics normalization

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
Top-level first_turn_isl now conflicts with sequence_distribution
(ValueError) instead of being silently ignored; each bucket carries
its own optional first_turn_isl for conversation-class seed context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
Each conversation draws one bucket at creation and keeps it for every
turn; per-bucket first_turn_isl sizes the seed context while isl sizes
per-turn growth. Single-turn draw ordering is byte-identical to the
previous per-turn sampling.

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
shared_system_length samples once per run (prompt identical across
sessions); user_context_length samples once per session. Scalar ints
still coerce to FixedDistribution; CLI flags unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
…ation

Plus first-turn docs coherence: per-bucket first_turn_isl examples and
the sequence_distribution conflict error documented in the tutorial.

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Try out this PR

Quick install:

pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@f5fa59348e472514ae1bd6d7090ecc42ff3dd23a

Recommended with virtual environment (using uv):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@f5fa59348e472514ae1bd6d7090ecc42ff3dd23a

Last updated for commit: f5fa593Browse code

@github-actions github-actions Bot added the feat label Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

@datadog-official

datadog-official Bot commented Jul 8, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 4 Pipeline jobs failed

Pre-commit | pre-commit   View in Datadog   GitHub Actions

Run Integration Tests | integration-tests (ubuntu-latest, 3.11)   View in Datadog   GitHub Actions

Run Integration Tests | integration-tests (ubuntu-latest, 3.12)   View in Datadog   GitHub Actions

View all 4 failed jobs.

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: f5fa593 | Docs | Give us feedback!

…n length

Virtual-history users derive turns_to_send from the dataset AVERAGE turn
count; with per-conversation turns distributions actually varying (fixed
on this branch), a user drawing a shorter conversation carried
num_turns beyond the conversation's real length and the worker errored
every such session at benchmark start. Latent on main, where the turns
distribution flattened to a constant so average == every length.

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
)
if not 0.0 <= self.probability <= 100.0:
raise ValueError(f"Probability must be in [0,100], got {self.probability}")
if self.probability < 0.0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy sequence-distribution parsing now accepts NaN/Inf weights because the new check only rejects negative values, which can poison cumulative probability normalization with non-finite values. Fix: reject non-finite weights and require the summed weight to be finite and positive.

🤖 AI Fix

Update src/aiperf/common/models/sequence_distribution.py so SequenceLengthPair.__post_init__ rejects not math.isfinite(self.probability) as well as negatives, and _validate_probability_weights rejects not math.isfinite(total_prob) before allowing normalization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f5fa593. SequenceLengthPair.__post_init__ now rejects non-finite probability and both stddevs, and _validate_probability_weights requires a finite positive total. Parser-level coverage added (256,128:inf / :nan rejected) plus construction-level nan/inf/-inf cases.

description="Relative probability weight for this distribution bucket (0-100). "
"Weights are normalized across all entries. "
"Example: probability=40 means 40%% of requests use this ISL/OSL.",
description="Relative probability weight for this distribution bucket. "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Typed YAML sequence_distribution buckets can accept infinite probability values, causing _TypedSequenceDistribution to compute inf / inf as NaN during sampling. Fix: disallow non-finite probabilities and validate that the total weight is finite and positive.

🤖 AI Fix

Update src/aiperf/config/types.py in SequenceDistributionEntry and validate_probability_distribution to reject non-finite probability values, for example by adding allow_inf_nan=False to the Field and checking math.isfinite(total) before total > 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f5fa593. SequenceDistributionEntry.probability now carries allow_inf_nan=False (inf was indeed accepted by ge=0.0; NaN was already rejected), and validate_probability_distribution rejects a total that overflows to inf even when every per-entry weight is finite (e.g. two 1e308 weights). Regression tests for both layers.

@ajcasagrande

Copy link
Copy Markdown
Contributor Author

Follow-up commit a9173ef: running the headline config end-to-end against the mock server surfaced a latent user-centric bug this branch exposes — virtual-history warm-start users derive their remaining-turn budget from the dataset average turn count, and with turns distributions now actually varying per conversation, users who drew a shorter-than-average conversation carried num_turns past the conversation's real length (worker rejected every such session at benchmark start: 22 errored requests out of 1,148 in a 200-session run). On main this could never fire because the turns distribution flattened to a constant. Fixed by clamping the warm-start budget to the drawn conversation's length; regression test with varying-length conversations added. Verified clean end-to-end re-run afterward.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: 6fbea411-8d49-40fd-b3e8-f7f1945c207a

📥 Commits

Reviewing files that changed from the base of the PR and between a9173ef and f5fa593.

📒 Files selected for processing (4)
  • src/aiperf/common/models/sequence_distribution.py
  • src/aiperf/config/types.py
  • tests/unit/common/models/test_sequence_distribution.py
  • tests/unit/config/test_distributions.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/unit/config/test_distributions.py
  • src/aiperf/config/types.py
  • tests/unit/common/models/test_sequence_distribution.py
  • src/aiperf/common/models/sequence_distribution.py

Walkthrough

This PR changes sequence-distribution probabilities to positive relative weights, adds PercentileDistribution support across config and schema, introduces first_turn_isl and typed prefix lengths, and updates composers and generators to sample directly from typed distributions.

Changes

Relative weights, percentile distribution, and typed sequence sampling

Layer / File(s) Summary
Relative-weight probability semantics
src/aiperf/common/models/sequence_distribution.py, tests/unit/common/models/test_sequence_distribution.py, docs/cli-options.md
Probability validation, normalization, statistics, parser handling, CLI text, and tests now use positive relative weights instead of a sum-to-100 percentage rule.
PercentileDistribution and schema wiring
src/aiperf/config/distributions.py, src/aiperf/config/schema/aiperf-config.schema.json, tests/unit/config/test_percentile_distribution.py, tests/unit/property/*
Adds PercentileDistribution with solving and sampling logic, wires it into the discriminated union and schema, and adds property-based coverage.
first_turn_isl and prefix-length config
src/aiperf/config/dataset/content.py, src/aiperf/config/types.py, docs/tutorials/*, tests/unit/config/*
Adds first_turn_isl, updates sequence-distribution docs, converts prefix prompt lengths to sampling distributions, and extends validation/tests.
Typed sequence sampling in composers
src/aiperf/dataset/composer/*.py, tests/unit/dataset/composer/*
Introduces sticky typed bucket sampling and rewires composers to draw ISL/OSL/turns/delay/passages directly from typed distributions.
Direct sampling in generators
src/aiperf/dataset/generator/{audio,image,prompt}.py, tests/unit/dataset/generator/*
Audio, image, and prompt generators now sample lengths/dimensions/context sizes directly via sample/sample_int instead of mean/stddev normal sampling.

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

Poem

A rabbit hopped through weights and buckets tall,
No more "sum to 100," relative wins it all! 🐇
Percentiles bloom where means once stood alone,
Sticky turns now share one bucket's tone.
First turns get their own special start—
Hop, sample, cache; this composer's a work of art! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main dataset/config changes, including typed sampling, percentile targets, sticky buckets, and first-turn context.
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.

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

🧹 Nitpick comments (3)
src/aiperf/common/models/sequence_distribution.py (1)

14-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: doc example simplifies the actual grammar.

Note: Probabilities are positive relative weights and do NOT need to sum to 100. The "50;1" shorthand in the example doesn't match the actual ISL,OSL:PROB grammar shown just above it, which could confuse readers trying to map the example to real syntax.

🤖 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 `@src/aiperf/common/models/sequence_distribution.py` around lines 14 - 20, The
docstring example in SequenceDistribution currently shows a shorthand that does
not match the actual semicolon grammar, which can mislead readers. Update the
examples in the sequence distribution documentation to use the real ISL,OSL:PROB
syntax consistently, and if you keep a normalization example, make sure it is
expressed with valid pair syntax in the relevant class or parser docs so the
examples align with the supported formats.
tests/unit/config/test_distributions.py (1)

573-579: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move local import to top of file.

DistributionParser is imported inside the test function; move it to the module-level imports.

♻️ Proposed fix
+from aiperf.common.models.sequence_distribution import DistributionParser
+
 def test_legacy_string_parser_accepts_relative_weights():
-    from aiperf.common.models.sequence_distribution import DistributionParser
-
     dist = DistributionParser.parse("100,10:50;4000,10:1")

As per path instructions, tests/**/*.{py,pyi}: "Keep imports at the top of the file, use fixtures for setup, and keep one focus per test."

🤖 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/unit/config/test_distributions.py` around lines 573 - 579, The test
`test_legacy_string_parser_accepts_relative_weights` imports
`DistributionParser` inside the function, but the module-level imports should be
used instead. Move the `DistributionParser` import to the top of the file with
the other imports, and keep the test body focused on parsing and sampling only.

Source: Path instructions

tests/unit/dataset/composer/test_distribution_wiring.py (1)

280-338: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move local imports to module top.

random_generator, SequenceDistributionEntry, _TypedSequenceDistribution, and validate_probability_distribution are imported inside three separate test methods in TestZeroWeightBuckets. Consolidate these into the top-level imports.

♻️ Proposed fix
+from aiperf.common import random_generator as rng
+from aiperf.config.types import (
+    SequenceDistributionEntry,
+    validate_probability_distribution,
+)
+from aiperf.dataset.composer.base import _TypedSequenceDistribution
+
 class TestZeroWeightBuckets:
     def test_zero_weight_bucket_validates_and_never_samples(self):
         """A probability: 0 bucket is valid config and is never sampled."""
-        from aiperf.common import random_generator as rng
-        from aiperf.config.types import SequenceDistributionEntry
-        from aiperf.dataset.composer.base import _TypedSequenceDistribution
-
         entries = [

(similar cleanup applies to the other two methods)

As per path instructions, tests/**/*.{py,pyi}: "Keep imports at the top of the file, use fixtures for setup, and keep one focus per test."

🤖 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/unit/dataset/composer/test_distribution_wiring.py` around lines 280 -
338, The TestZeroWeightBuckets methods are doing repeated local imports instead
of using the module-level imports expected in tests. Move random_generator,
SequenceDistributionEntry, _TypedSequenceDistribution, and
validate_probability_distribution to the top of the test module, then remove the
inline imports from test_zero_weight_bucket_validates_and_never_samples,
test_zero_weight_bucket_end_to_end_via_composer,
test_all_zero_list_rejected_by_validator, and
test_all_zero_list_rejected_by_typed_distribution so the tests only focus on
behavior.

Source: Path instructions

🤖 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 `@src/aiperf/config/schema/aiperf-config.schema.json`:
- Around line 9771-9773: The PeakEntry schema currently only allows the wrapped
PercentileDistribution shape, but the parser also accepts an inline {p50, p99,
weight} variant. Update the PeakEntry union in aiperf-config.schema.json to add
a sibling inline PercentileDistribution branch alongside the existing $ref,
using the same PercentileDistribution symbol so schema validation matches the
Python parser behavior.

In `@src/aiperf/dataset/composer/base.py`:
- Around line 71-84: The sample_lengths method in SequenceDistributionComposer
currently samples bucket.osl unconditionally, so a zero-valued OSL still becomes
at least 1 instead of disabling output. Update the bucketed path to mirror the
plain-path behavior by checking the OSL distribution’s expected value before
sampling, and return 0 for the OSL when it is disabled. Keep the fix localized
to sample_lengths and use the existing bucket.osl and isl_dist logic to preserve
first-turn behavior.

In `@tests/unit/config/test_percentile_distribution.py`:
- Line 69: The pytest.raises regex patterns in the percentile distribution tests
use normal strings, which triggers Ruff RUF043; update the match= arguments in
the pytest.raises blocks to raw string literals in the relevant test cases so
the regex alternation is preserved. Apply this to the assertions around the
infeasible/mean case and the too extreme/finite case in the percentile
distribution test module.

---

Nitpick comments:
In `@src/aiperf/common/models/sequence_distribution.py`:
- Around line 14-20: The docstring example in SequenceDistribution currently
shows a shorthand that does not match the actual semicolon grammar, which can
mislead readers. Update the examples in the sequence distribution documentation
to use the real ISL,OSL:PROB syntax consistently, and if you keep a
normalization example, make sure it is expressed with valid pair syntax in the
relevant class or parser docs so the examples align with the supported formats.

In `@tests/unit/config/test_distributions.py`:
- Around line 573-579: The test
`test_legacy_string_parser_accepts_relative_weights` imports
`DistributionParser` inside the function, but the module-level imports should be
used instead. Move the `DistributionParser` import to the top of the file with
the other imports, and keep the test body focused on parsing and sampling only.

In `@tests/unit/dataset/composer/test_distribution_wiring.py`:
- Around line 280-338: The TestZeroWeightBuckets methods are doing repeated
local imports instead of using the module-level imports expected in tests. Move
random_generator, SequenceDistributionEntry, _TypedSequenceDistribution, and
validate_probability_distribution to the top of the test module, then remove the
inline imports from test_zero_weight_bucket_validates_and_never_samples,
test_zero_weight_bucket_end_to_end_via_composer,
test_all_zero_list_rejected_by_validator, and
test_all_zero_list_rejected_by_typed_distribution so the tests only focus on
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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 6c273293-9e9d-4ccc-912a-a87cd11a8347

📥 Commits

Reviewing files that changed from the base of the PR and between ccbcfa7 and 4451b5a.

📒 Files selected for processing (31)
  • docs/cli-options.md
  • docs/tutorials/sequence-distributions.md
  • docs/tutorials/yaml-distributions.md
  • src/aiperf/common/models/sequence_distribution.py
  • src/aiperf/config/dataset/content.py
  • src/aiperf/config/distributions.py
  • src/aiperf/config/flags/cli_config.py
  • src/aiperf/config/schema/aiperf-config.schema.json
  • src/aiperf/config/types.py
  • src/aiperf/dataset/composer/base.py
  • src/aiperf/dataset/composer/synthetic.py
  • src/aiperf/dataset/composer/synthetic_rankings.py
  • src/aiperf/dataset/generator/audio.py
  • src/aiperf/dataset/generator/image.py
  • src/aiperf/dataset/generator/prompt.py
  • tests/unit/common/models/test_sequence_distribution.py
  • tests/unit/config/test_camel_case_config.py
  • tests/unit/config/test_distributions.py
  • tests/unit/config/test_percentile_distribution.py
  • tests/unit/dataset/composer/test_base_composer.py
  • tests/unit/dataset/composer/test_distribution_wiring.py
  • tests/unit/dataset/composer/test_percentile_e2e.py
  • tests/unit/dataset/composer/test_sticky_sequence_buckets.py
  • tests/unit/dataset/composer/test_synthetic_rankings_composer.py
  • tests/unit/dataset/generator/test_audio_generator.py
  • tests/unit/dataset/generator/test_image_generator.py
  • tests/unit/dataset/generator/test_prompt_generator.py
  • tests/unit/property/_numeric_bounds_baseline.txt
  • tests/unit/property/_strategies.py
  • tests/unit/property/test_finite_invariants.py
  • tests/unit/property/test_pydantic_field_fuzz.py

Comment on lines +9771 to +9773
{
"$ref": "#/$defs/PercentileDistribution"
},

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect PeakEntry parsing to confirm inline percentile shorthand is/ isn't supported.
rg -nP -C6 '\bclass PeakEntry\b|weight.*pop|extract.*weight' src/aiperf/common/models src/aiperf/config

Repository: ai-dynamo/aiperf

Length of output: 2428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the PeakEntry model and the relevant schema definitions.
sed -n '540,610p' src/aiperf/config/distributions.py
printf '\n--- schema excerpt ---\n'
sed -n '9680,9810p' src/aiperf/config/schema/aiperf-config.schema.json

Repository: ai-dynamo/aiperf

Length of output: 7527


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether PeakEntry schema already has an inline PercentileDistribution variant.
rg -n -C3 'PercentileDistribution peak|Inline Percentile|PeakEntry' src/aiperf/config/schema/aiperf-config.schema.json

Repository: ai-dynamo/aiperf

Length of output: 1383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the PeakEntry variants to see whether an inline percentile shorthand exists.
sed -n '9751,9865p' src/aiperf/config/schema/aiperf-config.schema.json

Repository: ai-dynamo/aiperf

Length of output: 4428


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the remaining PeakEntry anyOf variants and look for a percentile shorthand.
sed -n '9865,9965p' src/aiperf/config/schema/aiperf-config.schema.json

Repository: ai-dynamo/aiperf

Length of output: 3794


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the rest of PeakEntry's inline variants for a percentile shorthand.
sed -n '9965,10080p' src/aiperf/config/schema/aiperf-config.schema.json

Repository: ai-dynamo/aiperf

Length of output: 3938


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect SamplingDistribution and PercentileDistribution definitions.
rg -n -C4 'class PercentileDistribution|SamplingDistribution\s*=|class PeakEntry' src/aiperf/config/distributions.py

Repository: ai-dynamo/aiperf

Length of output: 1265


Add an inline PercentileDistribution peak variant
PeakEntry accepts inline {p50, p99, weight} in Python, but the schema only exposes the wrapper form. Add a sibling inline PercentileDistribution branch so schema validation matches the parser.

🤖 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 `@src/aiperf/config/schema/aiperf-config.schema.json` around lines 9771 - 9773,
The PeakEntry schema currently only allows the wrapped PercentileDistribution
shape, but the parser also accepts an inline {p50, p99, weight} variant. Update
the PeakEntry union in aiperf-config.schema.json to add a sibling inline
PercentileDistribution branch alongside the existing $ref, using the same
PercentileDistribution symbol so schema validation matches the Python parser
behavior.

Comment on lines +71 to +84
def sample_lengths(
self, bucket: SequenceDistributionEntry, *, is_first: bool = False
) -> tuple[int, int]:
"""Draw one (ISL, OSL) pair from the held bucket. The first turn's ISL
comes from the bucket's first_turn_isl (seed context) when set."""
isl_dist = (
bucket.first_turn_isl
if is_first and bucket.first_turn_isl is not None
else bucket.isl
)
return (
isl_dist.sample_int(self._rng),
bucket.osl.sample_int(self._rng),
)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm sample_int()'s floor-to-1 behavior is shared by all SamplingDistribution
# subtypes (Fixed/Normal/LogNormal/Multimodal/Empirical/Percentile) and not
# overridden anywhere to special-case zero.
rg -n "def sample_int" src/aiperf/config/distributions.py -A3

Repository: ai-dynamo/aiperf

Length of output: 291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant implementation and nearby helpers.
sed -n '1,220p' src/aiperf/dataset/composer/base.py
printf '\n--- distributions ---\n'
sed -n '1,260p' src/aiperf/config/distributions.py

Repository: ai-dynamo/aiperf

Length of output: 18690


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate any existing handling of zero-mean OSL or max_tokens disabling.
rg -n "expected_value > 0|osl: \{mean: 0\}|max_tokens=0|sample_lengths\(" src/aiperf -A3 -B3

Repository: ai-dynamo/aiperf

Length of output: 4735


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the turn-length selection path that uses the bucket sampler.
sed -n '228,320p' src/aiperf/dataset/composer/base.py

Repository: ai-dynamo/aiperf

Length of output: 3967


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the sequence-distribution config schema and any docs for OSL=0 semantics.
rg -n "SequenceDistributionEntry|first_turn_isl|osl:.*mean: 0|disables output|max_tokens" src/aiperf/config src/aiperf/dataset -A4 -B4

Repository: ai-dynamo/aiperf

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the exact turn-length logic around the bucket and plain paths.
sed -n '240,320p' src/aiperf/dataset/composer/base.py

Repository: ai-dynamo/aiperf

Length of output: 3296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the sequence distribution entry schema.
rg -n "class SequenceDistributionEntry|sequence_distribution" src/aiperf/config -A40 -B10

Repository: ai-dynamo/aiperf

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact distribution contract for zero-valued OSL and sampling semantics.
sed -n '1,220p' src/aiperf/config/types.py

Repository: ai-dynamo/aiperf

Length of output: 6285


src/aiperf/dataset/composer/base.py: guard bucketed OSL before sampling
bucket.osl.sample_int(self._rng) always floors to at least 1, so a bucket with osl: {mean: 0} still produces max_tokens=1 instead of disabling output like the plain path does. Mirror the plain-path expected_value > 0 guard here and return 0 when the bucket OSL is disabled.

🤖 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 `@src/aiperf/dataset/composer/base.py` around lines 71 - 84, The sample_lengths
method in SequenceDistributionComposer currently samples bucket.osl
unconditionally, so a zero-valued OSL still becomes at least 1 instead of
disabling output. Update the bucketed path to mirror the plain-path behavior by
checking the OSL distribution’s expected value before sampling, and return 0 for
the OSL when it is disabled. Keep the fix localized to sample_lengths and use
the existing bucket.osl and isl_dist logic to preserve first-turn behavior.

],
) # fmt: skip
def test_extreme_ratio_rejected(self, p50: float, p99: float) -> None:
with pytest.raises(ValueError, match="too extreme|finite"):

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 raw string for the regex match= pattern.

The | alternation is intended regex, but the non-raw string trips Ruff RUF043. Same applies to match="infeasible|mean" on Line 49.

🧹 Proposed fix
-        with pytest.raises(ValueError, match="too extreme|finite"):
+        with pytest.raises(ValueError, match=r"too extreme|finite"):
📝 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
with pytest.raises(ValueError, match="too extreme|finite"):
with pytest.raises(ValueError, match=r"too extreme|finite"):
🧰 Tools
🪛 Ruff (0.15.20)

[warning] 69-69: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

🤖 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/unit/config/test_percentile_distribution.py` at line 69, The
pytest.raises regex patterns in the percentile distribution tests use normal
strings, which triggers Ruff RUF043; update the match= arguments in the
pytest.raises blocks to raw string literals in the relevant test cases so the
regex alternation is preserved. Apply this to the assertions around the
infeasible/mean case and the too extreme/finite case in the percentile
distribution test module.

Source: Linters/SAST tools

@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.

🧹 Nitpick comments (1)
src/aiperf/timing/strategies/user_centric_rate.py (1)

181-196: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Clamp logic correctly enforces the conversation-length invariant.

conversation_turns = len(sampled.metadata.turns) and the resulting min(max_turns, conversation_turns) correctly prevents User.max_turns from exceeding the sampled conversation's actual length, matching the UserSessionManager contract that rejects num_turns > len(conversation.turns).

One minor defensive note: if max_turns else conversation_turns uses a truthy check rather than is not None. If max_turns=0 is ever passed by a future caller, it would silently fall back to conversation_turns instead of 0. Currently unreachable since the only caller passing max_turns guards turns_to_send <= 0 before calling this method, so this is safe today but fragile against future refactors.

🛡️ Defensive fix
-            max_turns=min(max_turns, conversation_turns)
-            if max_turns
-            else conversation_turns,
+            max_turns=min(max_turns, conversation_turns)
+            if max_turns is not None
+            else conversation_turns,
🤖 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 `@src/aiperf/timing/strategies/user_centric_rate.py` around lines 181 - 196, In
the user-centric rate strategy, the clamp in the user creation path is correct,
but the `max_turns` fallback in the `User` constructor call is fragile because
it uses a truthy check. Update the logic in the method that builds the `User`
instance so it distinguishes `None` from valid numeric values, preserving an
explicit `0` if it is ever passed while still clamping against
`len(sampled.metadata.turns)`.
🤖 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.

Nitpick comments:
In `@src/aiperf/timing/strategies/user_centric_rate.py`:
- Around line 181-196: In the user-centric rate strategy, the clamp in the user
creation path is correct, but the `max_turns` fallback in the `User` constructor
call is fragile because it uses a truthy check. Update the logic in the method
that builds the `User` instance so it distinguishes `None` from valid numeric
values, preserving an explicit `0` if it is ever passed while still clamping
against `len(sampled.metadata.turns)`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: e817a92f-57be-4e72-8c59-ac29addd8655

📥 Commits

Reviewing files that changed from the base of the PR and between 4451b5a and a9173ef.

📒 Files selected for processing (2)
  • src/aiperf/timing/strategies/user_centric_rate.py
  • tests/unit/timing/strategies/test_user_centric_rate.py

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Typed buckets: probability gains allow_inf_nan=False (inf/inf normalized
to NaN in the sampler); validate_probability_distribution also rejects a
sum that overflows to inf. Legacy string format: SequenceLengthPair
rejects NaN/inf probability and stddevs, and the total-weight check
requires a finite positive sum.

Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant