Skip to content

feat(nemotron-parse): richer inference metrics, engine_kwargs passthrough, fanout stage spec#2054

Merged
praateekmahajan merged 5 commits into
NVIDIA-NeMo:mainfrom
praateekmahajan:praateek/nemotron-parse-fixes
Jun 22, 2026
Merged

feat(nemotron-parse): richer inference metrics, engine_kwargs passthrough, fanout stage spec#2054
praateekmahajan merged 5 commits into
NVIDIA-NeMo:mainfrom
praateekmahajan:praateek/nemotron-parse-fixes

Conversation

@praateekmahajan

@praateekmahajan praateekmahajan commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Richer per-task metrics in NemotronParseInferenceStage (vLLM backend): now emits token counts (prompt + output), output character count, inference wall time, truncated/empty output counts, and retry count via _log_metrics so TaskPerfUtils can aggregate them across the pipeline run.

  • engine_kwargs passthrough: NemotronParseInferenceStage and create_vllm_llm now accept arbitrary extra engine kwargs (e.g. gpu_memory_utilization, max_num_batched_tokens) forwarded to vllm.LLM, eliminating the need to subclass for tuning.

  • Fanout stage spec on PDFPartitioningStage: marked as a fanout stage in ray_stage_spec to unlock page-level parallelism under the Ray executor.

  • vLLM v1 port-collision retry in create_vllm_llm: also retries on "Engine core initialization failed" — in vLLM v1 the distributed-store bind happens inside the EngineCore subprocess so the raw EADDRINUSE never reaches the caller. Added random jitter to de-stagger concurrent workers.

  • Benchmark script (nemotron_parse_pdf_benchmark.py): switched from manual stage-timing loops to TaskPerfUtils.aggregate_task_metrics, reporting pages/s and output-tokens/s derived from the new stage metrics.

Test plan

  • tests/stages/interleaved/pdf/nemotron_parse/test_stages.py — metrics emission, engine_kwargs forwarding, fanout spec
  • tests/utils/test_vllm_utils.py — wrapped vLLM v1 startup retry, extra kwargs forwarding

🤖 Generated with Claude Code

@praateekmahajan praateekmahajan requested a review from a team as a code owner June 5, 2026 22:15
@praateekmahajan praateekmahajan requested review from huvunvidia and removed request for a team June 5, 2026 22:15
@copy-pr-bot

copy-pr-bot Bot commented Jun 5, 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.

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR enriches NemotronParseInferenceStage with per-task vLLM metrics (token counts, wall time, retry counts), adds engine_kwargs passthrough to both the stage and create_vllm_llm, marks PDFPartitioningStage as a Ray fanout stage, and tightens the vLLM v1 port-collision retry logic with a non-retryable error blocklist and random jitter.

  • Metrics: _vllm_metrics_from_outputs collects prompt/output token counts, output character count, truncation flags, and retry count; all values flow through _log_metrics for TaskPerfUtils aggregation. The HF backend also receives a minimal metric set for consistency.
  • engine_kwargs passthrough: Both NemotronParseInferenceStage and create_vllm_llm now accept arbitrary extra kwargs merged after explicit defaults, allowing callers to tune gpu_memory_utilization, max_num_batched_tokens, etc. without subclassing.
  • Retry hardening: _exception_chain_text walks the full __cause__/__context__ chain, and a blocklist (_NON_RETRYABLE_MARKERS) prevents OOM/config errors from burning retry budget on wrapped startup failures.

Confidence Score: 5/5

Safe to merge; all previously flagged issues are resolved and the new code is well-tested.

The previously flagged problems (duplicate-kwarg TypeError, unreachable fallback return, and overly broad engine-startup retry markers) are all addressed. The engine_kwargs merge uses a plain dict with **-spread so Python's normal keyword-binding rules prevent duplicates. The unreachable path now raises RuntimeError. The retry classification walks the full exception chain and fast-fails on OOM markers. New tests cover metric emission, fanout spec, empty-output guard, kwargs forwarding, and the wrapped vLLM v1 startup path.

The benchmark script hardcodes the metric key prefix as a string literal tied to the stage's default name; if the stage is ever renamed or instantiated with a different name, throughput figures silently become zero.

Important Files Changed

Filename Overview
nemo_curator/stages/interleaved/pdf/nemotron_parse/inference.py Adds _vllm_metrics_from_outputs helper and per-task metric logging; engine_kwargs passthrough via merged dict; both previously-flagged issues (unreachable fallback, empty-outputs guard) are resolved
nemo_curator/utils/vllm_utils.py Adds _NON_RETRYABLE_MARKERS / _ENGINE_STARTUP_FAILURE_MARKERS with full exception-chain inspection; extra_engine_kwargs forwarded via **-spread after explicit defaults; jitter added to port-retry sleep
nemo_curator/stages/interleaved/pdf/nemotron_parse/partitioning.py Adds ray_stage_spec returning IS_FANOUT_STAGE=True to unlock page-level parallelism under the Ray executor
benchmarking/scripts/nemotron_parse_pdf_benchmark.py Replaces manual stage-timing loops with TaskPerfUtils.aggregate_task_metrics; hardcodes metric key prefix that silently returns 0.0 if the stage name is overridden
tests/stages/interleaved/pdf/nemotron_parse/test_stages.py Adds thorough tests for fanout spec, metrics emission, engine_kwargs override, empty-outputs guard, and the unreachable RuntimeError path
tests/utils/test_vllm_utils.py Adds tests for vLLM v1 wrapped startup retry, non-retryable OOM fast-fail, and extra kwargs forwarding to vllm.LLM

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant P as process()
    participant IV as _infer_vllm()
    participant VM as _vllm_metrics_from_outputs()
    participant LM as _log_metrics()

    P->>P: load images, log image_load_time
    P->>IV: valid_images
    loop up to 3 attempts
        IV->>IV: llm.generate(prompts)
        alt success
            IV-->>P: texts, raw_outputs, vllm_retries
        else RuntimeError
            IV->>IV: _reset_vllm() + increment vllm_retries
        end
    end
    P->>VM: raw_outputs + timing + page counts
    VM-->>P: metrics dict (tokens, chars, truncations, retries)
    P->>LM: merged metrics dict
    LM->>LM: update _custom_metrics (consumed by StageTimer)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant P as process()
    participant IV as _infer_vllm()
    participant VM as _vllm_metrics_from_outputs()
    participant LM as _log_metrics()

    P->>P: load images, log image_load_time
    P->>IV: valid_images
    loop up to 3 attempts
        IV->>IV: llm.generate(prompts)
        alt success
            IV-->>P: texts, raw_outputs, vllm_retries
        else RuntimeError
            IV->>IV: _reset_vllm() + increment vllm_retries
        end
    end
    P->>VM: raw_outputs + timing + page counts
    VM-->>P: metrics dict (tokens, chars, truncations, retries)
    P->>LM: merged metrics dict
    LM->>LM: update _custom_metrics (consumed by StageTimer)
Loading

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile

Comment on lines +38 to +49
_ENGINE_STARTUP_FAILURE_MARKERS = (
"EADDRINUSE",
"address already in use",
"Engine core initialization failed",
"EngineCore failed to start",
)


def _is_engine_startup_failure(exc: BaseException) -> bool:
"""Return True if ``exc`` looks like a retryable vLLM engine-startup failure."""
message = str(exc)
return any(marker in message for marker in _ENGINE_STARTUP_FAILURE_MARKERS)

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.

P1 Overly broad retry markers will swallow non-port-collision failures

"Engine core initialization failed" and "EngineCore failed to start" are the generic wrapper messages that vLLM emits when the EngineCore subprocess dies for any reason — including OOM during model loading, an invalid config, or CUDA errors. Matching these strings means that a legitimate non-retryable error (e.g., torch.cuda.OutOfMemoryError wrapped into one of these messages) will now be retried up to max_port_retries times, sleeping up to ~5 s per attempt before the real error finally surfaces. The four existing EADDRINUSE-specific markers are precise; the two new ones are not. A tighter heuristic (e.g., also requiring "EADDRINUSE" somewhere in the exception chain, or inspecting __cause__) would keep retries limited to actual port collisions.

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.

Agree with this one.

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.

Maybe something like this?

# Errors that should NEVER be retried (non-transient)
_NON_RETRYABLE_MARKERS = (
    "out of memory",
    "CUDA out of memory",
    "cudaErrorMemoryAllocation",
)

# Substrings that indicate a (usually transient) vLLM engine-startup failure
# caused by a MASTER_PORT collision.
_ENGINE_STARTUP_FAILURE_MARKERS = (
    "EADDRINUSE",
    "address already in use",
    "Engine core initialization failed",
    "EngineCore failed to start",
)

and

def _is_engine_startup_failure(exc: BaseException) -> bool:
    """Return True if ``exc`` looks like a retryable vLLM engine-startup failure."""
    message = str(exc)
    if any(marker in message for marker in _NON_RETRYABLE_MARKERS):
        return False  # OOM / config errors — fail fast, don't retry
    return any(marker in message for marker in _ENGINE_STARTUP_FAILURE_MARKERS)

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.

Yes, this is a clean improvement. The blocklist-first pattern is easy to reason about and directly addresses the concern — OOM errors fail fast instead of burning retry budget.

A couple of thoughts:

  1. The OOM markers may be incomplete. Other non-transient failures (e.g. invalid model config, RuntimeError: CUDA error: device-side assert triggered) would still slip through to the _ENGINE_STARTUP_FAILURE_MARKERS check and get retried. The blocklist approach only works well if it's kept up to date. A small comment acknowledging this would set expectations:
Suggested change
_ENGINE_STARTUP_FAILURE_MARKERS = (
"EADDRINUSE",
"address already in use",
"Engine core initialization failed",
"EngineCore failed to start",
)
def _is_engine_startup_failure(exc: BaseException) -> bool:
"""Return True if ``exc`` looks like a retryable vLLM engine-startup failure."""
message = str(exc)
return any(marker in message for marker in _ENGINE_STARTUP_FAILURE_MARKERS)
# Errors that should NEVER be retried (non-transient).
# Add new entries here if vLLM introduces other non-retryable startup errors.
_NON_RETRYABLE_MARKERS = (
"out of memory",
"CUDA out of memory",
"cudaErrorMemoryAllocation",
)
  1. str(exc) coverage: if vLLM constructs the wrapper message as "Engine core initialization failed" without embedding the original OOM text, the blocklist check on message would miss it. It may be worth also walking __cause__/__context__:
def _is_engine_startup_failure(exc: BaseException) -> bool:
    """Return True if ``exc`` looks like a retryable vLLM engine-startup failure."""
    # Collect message text from the full exception chain
    parts = []
    e: BaseException | None = exc
    while e is not None:
        parts.append(str(e))
        e = e.__cause__ or e.__context__
    message = " ".join(parts)

    if any(marker in message for marker in _NON_RETRYABLE_MARKERS):
        return False
    return any(marker in message for marker in _ENGINE_STARTUP_FAILURE_MARKERS)

This is optional — if vLLM reliably propagates the full traceback text into the outer exception string, the simpler version is fine. But the chain walk makes it more defensive.

Overall the proposed structure is a solid fix.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

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.

Added 👍

else:
texts = [output.outputs[0].text for output in outputs]
return texts, outputs, vllm_retries
return [], [], vllm_retries

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.

P2 Unreachable fallback return produces silent empty outputs

The final return [], [], vllm_retries is never reached: on the last retry attempt the except branch unconditionally raises, and every successful attempt returns from the else block. If this line were ever reached (e.g., after a future refactor changes the loop structure), it would silently return empty valid_outputs, causing the downstream process() to write empty text_content for every page rather than propagating an error. Consider replacing it with raise RuntimeError("unreachable") or removing it entirely, consistent with the pragma comment in create_vllm_llm.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@praateekmahajan praateekmahajan changed the title feat(nemotron-parse): Dynamo server inference mode, engine_kwargs passthrough, fanout stage feat(nemotron-parse): richer inference metrics, engine_kwargs passthrough, fanout stage spec Jun 5, 2026
@praateekmahajan

Copy link
Copy Markdown
Contributor Author

@claude review

Comment on lines +268 to +269
texts = [output.outputs[0].text for output in outputs]
return texts, outputs, vllm_retries

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.

P1 The _vllm_metrics_from_outputs helper correctly skips requests where req_out.outputs is empty, but the text extraction list-comprehension right below it in _infer_vllm accesses output.outputs[0].text unconditionally. If vLLM ever returns a RequestOutput with an empty completions list (e.g., during a cancelled or filtered request), the metrics path records the page as an empty output while the text path crashes with IndexError, propagating as an unhandled task failure. Using the same guard here makes the two paths consistent.

Suggested change
texts = [output.outputs[0].text for output in outputs]
return texts, outputs, vllm_retries
texts = [output.outputs[0].text if output.outputs else "" for output in outputs]
return texts, outputs, vllm_retries

…fanout stage spec

- Add Dynamo server inference mode to NemotronParseInferenceStage: routes requests
  to a running Dynamo HTTP server instead of launching an in-process vLLM engine
- Add engine_kwargs passthrough to NemotronParseInferenceStage and create_vllm_llm
  so callers can tune gpu_memory_utilization, max_num_batched_tokens, etc.
- Mark PDFPartitioningStage as a fanout stage via ray_stage_spec to enable
  parallelism across PDF pages
- Improve vLLM port-collision retry: handle wrapped vLLM v1 startup failures
  ("Engine core initialization failed") in addition to raw EADDRINUSE, and add
  random jitter so concurrent workers de-stagger instead of re-colliding
- Update PDF parsing benchmark script for new inference stage API
- Add tests covering all the above

Signed-off-by: Praateek <praateekm@gmail.com>
Signed-off-by: Praateek <praateekm@gmail.com>

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

Looks good. Thanks.

Signed-off-by: Praateek <praateekm@gmail.com>
…arse-fixes

Signed-off-by: Praateek <praateekm@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.

3 participants