Skip to content

Audio: add extended performance observability#2223

Draft
mohammadaaftabv wants to merge 47 commits into
NVIDIA-NeMo:mainfrom
mohammadaaftabv:aaftabv/pr1967-performance-followup
Draft

Audio: add extended performance observability#2223
mohammadaaftabv wants to merge 47 commits into
NVIDIA-NeMo:mainfrom
mohammadaaftabv:aaftabv/pr1967-performance-followup

Conversation

@mohammadaaftabv

@mohammadaaftabv mohammadaaftabv commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add backend-neutral actor/GPU identity and stage timing aggregation
  • collect GPU and pipeline hardware utilization without coupling to an ASR model
  • emit terminal audio summaries through the regular ManifestWriterStage
  • redact sensitive pipeline metadata before persistence

Scope boundary

This PR is performance-only. It contains no Qwen adapter, generic ASR implementation, sharded writer, payload lifecycle, dispatch envelope, or global planner.

Test plan

  • Ruff checks
  • Backend identity, GPU sampling, pipeline hardware, stage timing, and manifest summary tests (103 passed final focused run)
  • FastConformer-labelled model-neutral writer summary proof
  • Diff validated against latest authoritative main

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
- hoist followup_prompt default into _FOLLOWUP_PROMPT module constant
- move 6 lazy imports to module scope (torch, transformers.Qwen3OmniMoeProcessor,
  qwen_omni_utils.process_mm_info, huggingface_hub.{snapshot_download,
  hf_hub_download}, yaml); keep existing vllm try/except guard
- drop redundant `language and` short-circuit in _get_prompt_text
- guard tar.extractfile(...) against None before .read() in
  NemoTarShardReaderStage; add hard-link regression test
- make ShardedManifestWriterStage.output_dir a required field; drop empty-
  string post_init check
- add Apache/NVIDIA copyright headers to inference/__init__.py and
  qwen_omni_inprocess.yaml
- drop pip-fallback install block from tutorial README (uv-only)
- remove tests/stages/audio/inference/test_qwen_omni_tutorial.py per
  "no pytests for tutorials"
- retarget @patch decorators in test_qwen_omni.py to the use-site
  (nemo_curator.stages.audio.inference.qwen_omni.snapshot_download) so
  the patches still bind after the import hoist

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Add qwen-asr and its lazy-imported runtime companions to Curator's audio
extras so that the harvest.curator Docker image gets the full qwen-asr stack
via Curator's uv sync rather than via post-uv pip installs in NvLLMOps. This
honors the Algorithmic vs Data-Mover Dep Ownership Rule: algorithmic libraries
belong in Curator, NvLLMOps owns only data-mover clients.

audio_common gains the qwen-asr forced-aligner text-norm and audio-feature
companions that qwen-asr 0.0.6's qwen3_forced_aligner.py imports lazily:
nagisa==0.2.11, soynlp==0.0.493, pyarabic, opencc-python-reimplemented, and
nnAudio.

audio_cuda12 gains qwen-asr==0.0.6 itself for the Granary v2 Qwen-ASR
recovery stage, and fasttext==0.9.3 for the Granary v2 LID stage. fasttext
already lives in text_cpu but audio_cuda12 does not pull text_cpu, so the
declaration is duplicated here.

[tool.uv] override-dependencies replaces the broad huggingface-hub>=0.34,<1.0
override with three exact pins proven against qwen-asr by NvLLMOps commit
68f18e9b: transformers==4.57.6, accelerate==1.12.0, and
huggingface-hub==0.36.0. These force-override qwen-asr's declared
(incompatible) version pins so the resolver picks the proven-compatible
versions for the entire graph.

This change extends PR1967's scope from "first-stage Qwen-Omni inference
only" to also cover Granary v2 algorithmic-dep self-containment, so that
later Granary v2 PRs (Qwen-ASR recovery, text filtering, PnC, ITN, SED) can
rely on Curator's audio_cuda12 extra without further pip-after-uv overrides
in NvLLMOps.

Lock churn: +18 packages including qwen-asr 0.0.6, nagisa, soynlp, pyarabic,
opencc-python-reimplemented, nnaudio, and qwen-asr's gradio/flask transitive
demo deps. transformers/huggingface-hub/accelerate stayed at the
override-pinned versions, so no version drift for the qwen-omni stack.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
NemoTarShardDiscoveryStage and NemoTarredAudioReader previously declared
yaml_path as `yaml_path: str = ""` and validated it via a manual empty-
string check in __post_init__. Per reviewer feedback, this is cleaner as
a required dataclass field: __init__ enforces the requirement directly,
removing the sentinel and the manual check.

Reordered yaml_path before defaulted fields in both dataclasses (Python
requires non-default fields first). NemoTarShardDiscoveryStage's
__post_init__ existed only for the empty-string check and is removed;
NemoTarredAudioReader's __post_init__ keeps super().__init__() and
self._stages wiring, only the check is removed.

All three construction sites (the test, hydra _target_ resolution, and
NemoTarredAudioReader's own __post_init__) already use keyword arguments,
so the required-field reorder is transparent. The observable difference
for a caller that forgets yaml_path is now TypeError from __init__
(before any object exists) instead of ValueError from __post_init__.

Addresses:
- NVIDIA-NeMo#1967 (comment)
- NVIDIA-NeMo#1967 (comment)

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
…1.0 pins

The audio_cuda12 PR previously added three [tool.uv] override-dependencies
entries (huggingface-hub==0.36.0, transformers==4.57.6, accelerate==1.12.0)
described as overriding qwen-asr's incompatible declared pins. Audit of the
resolved graph showed that rationale was inaccurate:

- qwen-asr 0.0.6 declares transformers==4.57.6 / accelerate==1.12.0 natively
  in its requires-dist, so no override is needed for those.
- The remaining transformers/accelerate constraints across the graph
  (nemo-toolkit ~=4.57.0, vllm >=4.56.0,<5, pyannote-audio >=4.48.3,
  whisperx >=4.48.0) all naturally permit 4.57.6 / 1.12.0.
- The hf-hub conflict (data-designer-engine >=1.0.1 vs whisperx <1.0.0)
  was already resolved on main by hf-hub>=0.34,<1.0; this PR's tightening
  to ==0.36.0 was redundant.

All three entries removed; the pre-existing hf-hub override restored to
its original range. Verified via `uv lock` that the resolved versions of
transformers, accelerate, huggingface-hub, qwen-asr, nagisa, soynlp, vllm,
torch, torchaudio, torchvision, torchcodec, nixl-cu12, xgrammar, fasttext,
data-designer-engine, pyannote-audio, whisperx, nemo-toolkit, qwen-omni-utils,
fsspec, numpy, protobuf, setuptools are byte-identical before and after
(591 packages resolved). This PR now adds zero new override-dependencies
entries, eliminating the cross-modality blast radius that was flagged.

Also softened the remaining hard `==` pins inside audio_common /
audio_cuda12:

- nagisa ==0.2.11 -> >=0.2.11,<0.3
  (8-year-old stable lib; qwen-asr still transitively pins 0.2.11)
- qwen-asr ==0.0.6 -> >=0.0.6,<0.1
  (only 0.0.6 exists today, but a future security-patch 0.0.7 auto-flows
  in if/when published; <0.1 caps speculative jumps)
- fasttext ==0.9.3 -> >=0.9.3,<0.10
  (10-year-old maintenance-mode lib; releases every 2-4 years)

The 7-line preamble comment above qwen-asr was trimmed to one line in
the same edit (reviewer asked for less verbose).

uv.lock is regenerated; all resolved package versions remain identical
to the pre-edit state.

Addresses:
- NVIDIA-NeMo#1967 (comment)
- NVIDIA-NeMo#1967 (comment)
- NVIDIA-NeMo#1967 (comment)
- NVIDIA-NeMo#1967 (comment)
- NVIDIA-NeMo#1967 (comment)

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
…scaling

Per Praateek's review feedback on tutorials/audio/qwen_omni_inprocess/
qwen_omni_inprocess.yaml (NVIDIA-NeMo#1967), controlled-throughput benchmarking is
not a tutorial concern. Remove the explicit worker-count overrides
(reader_num_workers, reader_num_workers_per_node, omni_num_workers)
from the tutorial config and the NemoTarredAudioReader /
NemoTarShardReaderStage / InferenceQwenOmniStage classes. The
ProcessingStage base class defaults (num_workers() -> None,
xenna_stage_spec() -> {}) let Xenna's autoscaler size each stage from
the cluster resources, which is the documented happy-path behaviour
of the audio pipelines.

Changes:
- tutorials/audio/qwen_omni_inprocess/qwen_omni_inprocess.yaml:
  remove the MULTI-NODE SCALING block (reader_num_workers,
  reader_num_workers_per_node, omni_num_workers) and the three
  Hydra interpolations that fed them into the reader and qwen-omni
  stages.
- nemo_curator/stages/audio/io/nemo_tarred_reader.py: drop
  reader_num_workers / reader_num_workers_per_node fields on the
  composite NemoTarredAudioReader; drop num_workers_override /
  num_workers_per_node fields, their __post_init__ validation, the
  num_workers() override, and the xenna_stage_spec() override on
  NemoTarShardReaderStage.
- nemo_curator/stages/audio/inference/qwen_omni.py: drop the
  num_workers_override field plus the num_workers() and
  xenna_stage_spec() overrides on InferenceQwenOmniStage. Remove
  the now-unused `Any` import.
- tutorials/audio/qwen_omni_inprocess/README.md: drop the
  reader_num_workers / reader_num_workers_per_node / omni_num_workers
  rows from the configuration-knob table.
- tests/stages/audio/inference/test_qwen_omni.py: remove
  test_worker_override_specs (no longer applicable).

Addresses:
- NVIDIA-NeMo#1967 (comment)

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Refactors `nemo_curator/stages/audio/metrics/performance.py` around three
typed dataclasses and lifts every post-processing helper into a new module
`nemo_curator/stages/audio/metrics/performance_utils.py`. Fixes four
known anomalies in `perf_summary_merged.json` that the protocol-doc
3-way comparison flagged on the previous validation run (ac6598e).

Anomaly fixes
=============

P0 — `perf_invocations_counted = 0` (dedup wiring dead-code branch).

  Root cause: backends attach the same upstream `StagePerfStats` chain to
  every downstream task an actor invocation emits. The audio summary's
  dedup branch in `record_stage_perf` keyed on
  `StagePerfStats.invocation_id`, but `BaseStageAdapter` never populates
  that field, so the branch was dead and every perf record got summed
  once per emitted task.

  Fix (in `performance.py:record_stage_perf`): prefer
  `StagePerfStats.invocation_id` when the framework wires it, otherwise
  fall back to a deterministic value-tuple fingerprint
  (`_fingerprint_perf`) over (stage_name, process_time, actor_idle_time,
  num_items_processed, sorted custom-metric tuple). Repeat sightings of
  the same `StagePerfStats` produce byte-equal fingerprints → dedup
  correctly; two genuinely distinct invocations producing byte-equal
  tuples is astronomically unlikely.

A2 — reader `custom_metrics_sum` over-counted by the fanout factor.

  Same root cause as P0. With the fingerprint fallback active, the
  reader's invocation_count collapses from ~68 k (every emitted
  utterance triple-counts the source invocation) to ~8 (one per unique
  shard), and `custom_metrics_sum.output_utterances` collapses from
  ~592 M to ~68 k, matching the top-level `total_utterances`. All
  per-process_s throughput ratios stay numerically identical because
  both numerator and denominator were scaled by the same factor.

A4 — discovery `total_items_processed = 0` despite work being done.

  `NemoTarShardDiscoveryStage` is an actor-pattern stage: it synthesises
  output tasks from configuration (it doesn't consume one input task
  per output), so the framework's generic `num_items_processed` counter
  records zero. Adds an explicit `total_items_emitted` custom metric to
  the discovery stage's `_log_metrics` payload (audio-local; no
  framework change needed). The summary builder
  (`_build_stage_summary`) falls back to that value when the
  framework's `num_items_processed` is zero, so
  `throughput_items_per_s` and friends become meaningful for actor
  stages. A3 (`total_input_data_size_mb` mis-naming) remains
  intentionally out of scope — framework-level fix, recorded in
  protocol doc.

A5 — QwenOmni `inference_time` duplicate + redundant `model_<key>`
aliases.

  In `qwen_omni.py:process_batch` the metrics dict had both
  `inference_time_s` and `inference_time` for the same value, and the
  `model_<name>` alias loop emitted `model_audio_duration_s`,
  `model_waveform_bytes`, `model_output_tokens` etc. on top of the
  identical base-name keys the stage already emits. Drops the
  `inference_time` legacy key (kept `inference_time_s`); the alias loop
  now skips any name already present in `metrics`. Net: 6-7 fewer
  redundant keys per actor per run; model-side unique counters
  (`model_turn1_generation_time_s`, `model_turn1_prep_time_s`,
  `model_turn1_valid_inputs`, `model_utterances_skipped_preprocess`,
  turn-2 variants) are preserved.

Refactor
========

* New `AudioStageMetrics` dataclass — superset of every scalar custom
  metric any audio stage emits across the codebase (~70 typed fields
  covering reader, discovery, inference, ALM, split/merge, diarization,
  VAD, writer) plus a forward-compat `extras: dict[str, float]` for
  any future emitted key. `from_dict()` / `to_dict()` round-trip
  cleanly; zeros are stripped from the published JSON so empty fields
  don't bloat the artifact.

* New `AudioStageSamples` dataclass — per-invocation sample lists used
  to derive p50/p95 percentiles for batch_size, audio_duration_s,
  invocation_process_time_s, queue_wait_s (from `actor_idle_time`).
  Populated once per dedup'd invocation.

* New `AudioStageCallerContext` dataclass — caller-provided fields the
  audio accumulator cannot derive itself (actor_count_samples,
  gpu_util_pct_samples, gpu_hours, setup_time_s_total, wallclock_s).
  Optional; a writer with NVML / DCGM / Xenna-autoscaler snapshots
  passes these in via `build_summary(stage_caller_context=…)`. Missing
  → field omitted from the published JSON (no misleading zeros).

* `build_summary()` now also accepts `run_id` and `executor` kwargs and
  emits both the proposed pipeline-perf top-level shape (run_id,
  executor, input_hours, output_hours, rows_in, rows_out) AND the
  backward-compat keys the protocol-doc baselines depend on
  (total_utterances, total_audio_seconds, total_audio_hours,
  writer_wall_time_s, pipeline_audio_s_per_wall_s,
  pipeline_utterances_per_wall_s, perf_invocations_counted, shards,
  stages). The writer call site
  (`sharded_manifest_writer.py:_emit_summary`) does not need to
  change; new kwargs default to None / empty string.

* `total_input_data_size_mb` is removed from the per-stage summary
  output (it was a framework-level message-size metric, misleading at
  the audio level — see A3 in protocol doc).

`performance_utils.py` (new)
============================

Pure post-processing module — every audio-specific maths lives here so
it can be unit-tested in isolation and reused by anything that consumes
the perf JSON (CI throughput checks, Kratos harvesters, dashboards)
without dragging the full accumulator in.

Public surface:
  - `seconds_to_hours`, `hours_to_seconds`, `bytes_to_mb`, `bytes_to_gb`
  - `safe_ratio(num, denom)` — returns None when either ≤ 0 (consumer
    omits the field rather than carrying a misleading zero)
  - `add_ratio(entry, name, num, denom)` — writes to a dict only on
    positive result
  - `percentile(values, p)` — linear-interpolation percentile, no numpy
    dependency (works in writer pods without numpy on the import path)
  - `summarize_samples(values, name, percentiles)` — returns
    `{f"{name}_p50": …, f"{name}_p95": …}`
  - `audio_hours_per_gpu_hour(audio_s, gpu_s)` — domain composite for
    the proposed pipeline-perf shape
  - `items_per_hour(items, wall_s)` — generic throughput
  - `estimate_wallclock_s(total_proc, actor_count?, invocation_times?)`
    — best-effort stage wall: total_proc/actor_count → max(invocations)
    → fallback to CPU sum

Validation
==========

The next Kratos validation run will record (per protocol doc):
  * P0:  perf_invocations_counted  was 0  → must be > 0.
  * A2:  reader.invocation_count   was 68k → must be ≈ 8 (per shard).
  * A2:  reader.custom_metrics_sum.output_utterances was 592 M → must
         be ≈ 68 819 (matches total_utterances).
  * A4:  discovery.total_items_emitted was missing → must be ≈ 550 552;
         total_items_processed must fall back to that value.
  * A5:  QwenOmni custom_metrics_sum must NOT contain `inference_time`,
         `model_audio_duration_s`, `model_waveform_bytes`,
         `model_output_tokens`, `model_turn1_output_tokens`,
         `model_turn2_output_tokens`, `model_utterances_input`.
  * Throughput-neutral on QwenOmni_inference (|delta| ≤ 0.70 % vs
    ac6598e baseline).

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Bring the SDP-V2 design doc's stage->adapter split (see "Replaceability:
Stage -> Adapter" in the design doc) into PR NVIDIA-NeMo#1967 for the Qwen-Omni
first-pass ASR slot. Pure refactor: no behaviour change, no metric-key
change, no runtime-performance change expected.

Layout changes
--------------
New:
  nemo_curator/adapters/__init__.py
  nemo_curator/adapters/asr/__init__.py
  nemo_curator/adapters/asr/base.py     - ASRAdapter Protocol + ASRResult dataclass
  nemo_curator/adapters/asr/qwen_omni.py - QwenOmniASRAdapter (vLLM setup,
      two-turn generate; core inference logic moved verbatim from the
      deleted nemo_curator/models/qwen_omni.py)
  nemo_curator/stages/audio/inference/asr/stage.py - generic ASRStage
      matching the design doc shape (Tier-1 adapter_target + model_id +
      stage knobs; Tier-2 opaque adapter_kwargs; adapter class resolved
      at setup() via hydra.utils.get_class, matching Curator framework
      convention in nemo_curator/config/run.py).

Deleted:
  nemo_curator/stages/audio/inference/qwen_omni.py - replaced by ASRStage.
  nemo_curator/models/qwen_omni.py - body moved into QwenOmniASRAdapter.

Other audio inference stages (asr_nemo.py, sortformer, pyannote,
whisperx_vad, vad_segmentation, nemo_asr_align) are intentionally left
unchanged - the doc's broader stage-adapter rollout is a follow-up PR
series, not in PR NVIDIA-NeMo#1967's scope.

YAML / tutorial
---------------
tutorials/audio/qwen_omni_inprocess/qwen_omni_inprocess.yaml rewritten
to the design-doc shape: _target_ now ASRStage, with new top-level
adapter_target field (Tier-1 swap line) and an adapter_kwargs block
(Tier-2 opaque) carrying the Qwen-Omni-specific knobs.

The stage's name is explicitly overridden to QwenOmni_inference so
perf_summary_merged.json's stages[QwenOmni_inference] key stays stable
across the refactor, allowing direct apples-to-apples comparison
against the existing 4e8021a / ac6598e / d8b1d751 / 8cd168ea
baselines.

Tests
-----
tests/stages/audio/inference/test_qwen_omni.py rewritten end-to-end:
21 tests covering both ASRStage (process_batch contract, language
resolution, skip handling, A5-alias-dedup metric filter preservation,
adapter_target validation, outputs() with/without secondary_text_key,
setup() via mocked hydra.utils.get_class, setup_on_node prefetch
behaviour) and QwenOmniASRAdapter (transcribe_batch packaging
contract, single-turn drops secondary_text, empty-vLLM-output helpers).

Behaviour preservation
----------------------
Verified equivalent to pre-refactor for every observable surface:
- task.data output keys (qwen3_prediction_s1, qwen3_prediction_s2,
  _skip_me, waveform drop on keep_waveform=False)
- vLLM LLM(...) ctor args + generate() call sequence (moved verbatim)
- Turn-1 + Turn-2 disfluency flow (gated by followup_prompt)
- setup_on_node snapshot_download weight prefetch
- Metric key shape including A5-fix alias-dedup filter
- All P0 / A2 / A4 / A5 anomaly fixes from earlier commits

Throughput-neutrality will be Kratos-validated next.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
…eform, vLLM knobs, batch policy)

Builds on the 3c723c3 structural stage-adapter split. Brings the
Qwen-Omni ASR stage into full compliance with the SDP-V2 design doc
sections §6 (Qwen-Omni stage) and §0.3 (duration-bucketed batching).

Doc deltas implemented
----------------------
(b) Stage-side pre-slice + stitch-back  (§6 lines 765, 827).
    Any task whose audio duration exceeds the stage's per-turn cap is
    cut into contiguous, no-overlap, no-padding sub-chunks BEFORE the
    adapter sees it. The adapter still gets a flat list of items. The
    stage carries a parent-task index per item and joins the per-chunk
    text outputs back with a single-space join post-inference, so each
    input task still maps to exactly one output row.

    A new sub_chunks_generated metric surfaces the fan-out factor for
    transparency. utterances_input / utterances_processed continue to
    count PARENT tasks - the metric semantics downstream consumers rely
    on do not change.

    The parent task is marked skipped (_skip_me="empty_audio") only if
    EVERY sub-chunk was skipped; partial success keeps the parent live.

(c) keep_waveform default flipped to True  (§6 line 791).
    The granary-v2 default of False forced the §7 hallucination filter,
    §8 ASR-recovery, and §14 forced-alignment stages to re-decode the
    waveform from tarred shards. The SDP-V2 default lets them reuse the
    in-memory buffer the stage already holds. Tutorial YAML updated.

(d) Elevated 4 vLLM / cache knobs to adapter_kwargs  (§6 line 793).
    enable_prefix_caching (default True), prefix_caching_hash_algo
    (default "xxhash"), limit_mm_per_prompt_audio (default 2), and
    seed (default 1234) are now dataclass fields on the adapter and
    are threaded into the LLM(...) constructor. Defaults reproduce the
    pre-elevation behaviour byte-for-byte; deployments that want to
    flip the toggles no longer need a fresh image build.

(§0.3) Best-effort within-call duration-bucketed batching.
    New BatchPolicy dataclass at nemo_curator/stages/audio/batch_policy.py.
    Left-edge bucketing matches the doc-literal buckets_sec layout.
    bucketize() re-partitions whatever item list a process_batch call
    assembles into bucket-homogeneous sub-batches honouring both the
    per-bucket item cap and a global audio-second budget, so a single
    vLLM call never mixes a 40-min sub-chunk with 5-sec sub-chunks.
    Cross-process_batch queueing (the full §0.3 model with a per-bucket
    flush timer) requires a Curator-framework scheduler hook and is a
    follow-up PR; flush_interval_ms is recorded on the dataclass for
    forward-compat.

Naming + Tier-1 fields
----------------------
* secondary_text_key -> disfluency_text_key on ASRStage. Matches the
  doc §6 line 784 YAML knob name verbatim. ASRResult.secondary_text
  (the protocol-level field name) is unchanged - only the stage's
  user-facing knob renames.
* New Tier-1 stage fields: ideal_inference_segment_s (defaults to
  2400 s = 40 min, the Qwen-Omni per-turn cap from the doc), and
  max_inference_duration_s (defaults to ideal). Validation enforces
  max <= ideal so the bucket shape always stays anchored on ideal.
* Tutorial YAML also exposes the new top-level revision field so the
  HuggingFace model can be pinned per-deployment.

YAML + README
-------------
tutorials/audio/qwen_omni_inprocess/qwen_omni_inprocess.yaml gains the
batch_policy: block with the doc-literal shape
  buckets_sec: [0, 600, 1200, 2400]
  max_items_per_batch_by_bucket: [32, 16, 8, 4]
  max_audio_sec_per_batch: 2400
  flush_interval_ms: 250
Tutorial README documents every new knob in the existing parameter
table and adds a dedicated "Duration-aware bucketed batching" section.

Tests
-----
24 new tests added (21 -> 45 total) covering:
  - pre-slice fan-out for an over-long clip ([30,30,30,5] for 95 s)
  - stitch-back single-space join on pred_text + disfluency_text
  - partial-skip vs all-skip parent labelling
  - utterances_input counts parents; sub_chunks_generated counts items
  - BatchPolicy bucket-homogeneous sub-batches, item-cap splits,
    audio-sec-cap splits, no-policy single-call fallthrough
  - BatchPolicy.bucket_for boundary semantics
  - BatchPolicy strategy/length validation rejects bad configs
  - new adapter knobs as dataclass fields; default values match doc
  - new adapter knobs threaded into vLLM LLM(...) ctor
  - keep_waveform=True default kept; keep_waveform=False still drops
  - max_inference_duration_s <= ideal_inference_segment_s validation

Behaviour preservation
----------------------
All defaults reproduce the pre-elevation Qwen-Omni behaviour byte-for-
byte: enable_prefix_caching=True, prefix_caching_hash_algo="xxhash",
limit_mm_per_prompt_audio=2, seed=1234. A clip shorter than
max_inference_duration_s is NOT sliced (single sub-chunk passes through
verbatim). batch_policy=None reproduces the pre-§0.3 single-adapter-call
shape. The throughput-neutrality of all four deltas will be Kratos-
validated next.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Make adapter_target and model_id required on ASRStage, fix setup_on_node
prefetch to call _adapter_class(), forward revision into vLLM and the
processor in QwenOmniASRAdapter.setup(), guard optional audio_cuda12 imports
with lazy asr package exports, skip bad manifest lines instead of failing the
shard, and unpin fasttext in audio_cuda12. Expand unit tests (55-pass suite).

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Reword docstrings, tests, and tutorial docs so they describe behavior
without design-doc section numbers or build-history framing.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
…s/inference

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Refresh the audio docs for the new inference architecture and remove the
last back-compat surface for the relocated batch policy:

- Audio stages developer guide (nemo_curator/stages/audio/README.md):
  new section on the generic BucketedInferenceStage, the ASRStage +
  ASRAdapter (Tier-1/Tier-2) split, and the canonical
  stages/inference/batch_policy home, plus a "which base to use" table.
- Qwen-Omni tutorial README + audio tutorials index: surface the
  swappable-adapter ASR path and duration-bucketed batching.
- Point the tutorial YAML batch_policy _target_ at the canonical
  nemo_curator.stages.inference.batch_policy.BatchPolicy.
- Delete nemo_curator/stages/audio/batch_policy.py (the re-export shim);
  there is now a single canonical import path, no back-compat alias.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Address the latest PR NVIDIA-NeMo#1967 review threads:

- Modularize Turn-1/Turn-2 vLLM inference (Sushmitha, r3354153072):
  extract _pack_vllm_inputs() for the shared prompt/mm-data packing and
  _infer_turn() for the shared generate -> count-tokens -> scatter-by-index
  sequence. _prepare_single / _prepare_turn2_single and _run_two_turn now
  reuse these helpers, removing the duplicated turn logic. Behavior is
  preserved; covered by new unit tests.

- Fail loud on a broken vLLM engine contract (Greptile P1, r3354172564):
  switch the output-scatter zip() in _infer_turn to strict=True. A count
  mismatch between inputs and engine outputs now raises ValueError instead
  of silently emitting empty transcriptions with skipped=False. Added
  test_qwen_adapter_infer_turn_raises_on_vllm_count_mismatch and
  test_qwen_adapter_infer_turn_scatters_outputs_by_index.

- Empty the package-marker __init__ files per convention
  (Sarah, r3357852783 / r3357870309): nemo_curator/adapters/__init__.py
  and nemo_curator/stages/inference/__init__.py.

- Remove the duplicated "vLLM or Qwen utilities are missing" install
  section from the tutorial README (Sarah, r3357891807).

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Stamp actor/node/GPU identity via allocation-first resolution (Xenna
allocation, Ray get_gpu_ids, gated CVD fallback). Expand perf_summary with
per_gpu scheduling breakdown and pipeline_throughput. Harden tutorial
resources overrides (no open-ended hydra instantiate) and discovery YAML
structure validation. Reorganize tests by package path; update audio READMEs.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Explain that BaseStageAdapter meters every CPU and GPU stage onto
task._stage_perf, ShardedManifestWriterStage (num_workers=1) serializes
{shard}_perf.jsonl and perf_summary.json, write_perf_stats toggle,
_log_metrics author guide, and CPU vs GPU published JSON differences.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Replace internal milestone labels with public-facing perf terminology and
remove Kratos/NvLLMOps/harvest-specific validation wording from READMEs,
comments, and tests touched by PR NVIDIA-NeMo#1967.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Move actor/node/gpu label resolution into backends/perf_identity.py with
separate Xenna (allocation-first) and Ray (get_gpu_ids only) builders.
Each adapter stamps WorkerMetadata at setup; BaseStageAdapter copies
those fields verbatim. Removes CUDA_VISIBLE_DEVICES and cross-backend
fallback chain from base.py.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Split Ray label resolution into small helpers to avoid C901 complexity.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Document Xenna vs Ray resolution paths via backends/perf_identity.py
without cross-backend fallback.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Keep gpu_id as the within-run per_gpu join key while stamping
physical_address, pod_ip, hostname, gpu_indices, and gpu_uuids at
worker setup for cluster-debuggable perf summaries.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Clarify WorkerPerfIdentity scheduling vs cluster-location metadata
and the new per_gpu fields (physical_address, pod_ip, gpu_indices).

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
…tches

Ray Data delivers task batches as numpy ndarrays; truthiness on ndarrays
raises ValueError in BucketedInferenceStage. Coerce to list at the Ray
adapter boundary and use len(tasks)==0 in the shared bucketed base.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
… fixes

- ASRStage: Tier-1 xenna_num_workers / xenna_num_workers_per_node pin
  (mutually exclusive) to skip the autoscale cold-start ramp on the
  expensive GPU stage; surfaced via num_workers() (Ray Data) and
  xenna_stage_spec() (Xenna).
- NemoTarShardReaderStage: pin to 1 worker (num_workers + IS_ACTOR_STAGE
  + per-node Xenna spec) for bounded memory under keep_waveform.
- BatchPolicy: default max_audio_sec_per_batch 480 -> 2400 to match the
  Qwen-Omni docstring buckets.
- qwen_omni tutorial: drop driver-side HF_TOKEN/prefetch (does not reach
  remote workers); rename omni_num_workers* -> asr_num_workers* (Tier-1,
  adapter-agnostic) with asr_num_workers_per_node: 2.
- docs: "pin the GPU stage, autoscale the cheap stages" guidance plus
  adapter-swap GPU math.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
…ng, perf per-actor/per-GPU, reader/writer hardening

Adapters + inference co-location (review NVIDIA-NeMo#3/NVIDIA-NeMo#25):
- Move nemo_curator/adapters/asr/* -> stages/audio/inference/asr/adapters/*
- Move stages/inference/{batch_policy,bucketed_stage}.py -> stages/audio/inference/
- Update all imports, YAML adapter_target / batch_policy._target_, READMEs, tests
  (tests mirror the new package paths).

vLLM teardown (review NVIDIA-NeMo#15):
- _cleanup_gpu() no longer calls torch.distributed.destroy_process_group(); vLLM
  owns its TP group, so tearing down the global PG could corrupt other components.

Tutorial backend (review NVIDIA-NeMo#30):
- Default backend -> ray_data (YAML + main.py); trim the "Choosing a Backend" section.
- Drop tutorial test tests/tutorials/.../test_main_resources.py (review NVIDIA-NeMo#28).

Performance reporting:
- Unify metrics into a single per_actor block keyed by actor_id; GPU actors embed
  physical_address (host_ip:indices, now the canonical per-GPU key), gpu_indices/
  gpu_uuids, and per-GPU utilization percentiles (nested gpus map).
- Add first-class StagePerfStats.invocation_id for exact dedup; __add__ preserves
  identity only within the same worker.
- Add inference_compute_fraction (inference time / total process time).
- New GpuUtilSampler (NVML, bounded deque + lock); harden _collect_gpu_uuids to map
  physical indices -> visible ordinals under CUDA_VISIBLE_DEVICES.
- Remove per-shard <output_dir>/<shard_key>_perf.jsonl output.

Reader (nemo_tarred_reader):
- Collision-safe _ManifestIndex (exact path, then longest path-suffix; ambiguous
  basenames resolve to no-match).
- Enforce max_duration_s against decoded audio duration; narrow decode except to
  audio errors; write .done for empty/fully-filtered shards.

Writer (sharded_manifest_writer):
- Batch manifest writes per shard instead of per-utterance open/close.

Misc:
- Remove redundant super().setup_on_node() in RayActorPoolStageAdapter.__init__
  (executor already runs once-per-node setup).
- Fix stale docstring (disfluency_text_key); drop bucketize redundancy; trim
  adapter duplicate volume math; reduce comment verbosity across PR files.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
mohammadaaftabv and others added 15 commits July 12, 2026 19:57
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Add a raw-manifest Qwen-Omni tutorial that reuses the generic Hydra runner
with a raw-audio stage graph:

ManifestReader -> ResampleAudioStage -> MonoConversionStage -> ASRStage ->
ShardedManifestWriterStage.

Stamp shard metadata in ManifestReaderStage so raw JSONL input shards can
complete through ShardedManifestWriterStage and rebuild final manifests.
Add tests covering raw reader shard keys and totals.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Emit 1D numpy waveforms from MonoConversionStage when explicitly requested,
then opt the raw Qwen pipeline into that format so it matches the working
NeMo tar shard reader contract without changing Qwen Omni or ASR inference code.

Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Aaftab V <aaftabv@nvidia.com>
Signed-off-by: Mohammad Aaftab <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Keep the generic ASR adapter split and reference sharded I/O while removing superseded bucketing, NeMo ASR, and tutorial surfaces.

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Keep the adapter and observability functionality focused by dropping config redaction, model-input segmentation, mono-conversion extensions, and organization-only files from the PR surface.

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Prevent serialization failures and decode errors from corrupting shard completion, isolate malformed discovery entries, and keep unsupported-only ASR metrics accurate.

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Reuse the normal writer metrics path so tarred pipelines preserve actor, GPU, and hardware evidence in their terminal artifacts.

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Keep PR1967 focused on functional inference and sharded I/O while preserving the observability work for a dedicated follow-up.

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
Restore backend identity, GPU sampling, audio summaries, and manifest writer performance output as a dedicated follow-up to the core Qwen ASR pipeline.

Signed-off-by: aaftaabv@gmail.com <aaftaabv@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 17, 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.

Keep backend and pipeline metrics independent of model implementation while emitting terminal summaries through the regular audio manifest writer.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant