Skip to content

Add Omni-Fuse Tutorial - #2069

Open
hk1510 wants to merge 14 commits into
NVIDIA-NeMo:mainfrom
hk1510:main
Open

hk1510 wants to merge 14 commits into
NVIDIA-NeMo:mainfrom
hk1510:main

Conversation

@hk1510

@hk1510 hk1510 commented Jun 11, 2026

Copy link
Copy Markdown

Description

This PR adds a tutorial for Omni-Fuse. It follows a similar structure to existing tutorials and builds a curator pipeline to perform retrieval-based data curation for multimodal datasets.

Usage

Instructions to run the tutorial can be found in the README.md file. Open to feedback/adding additional instructions.

Checklist

  • I am familiar with the Contributing Guide.
  • New or Existing tests cover these changes.
  • The documentation is up to date with these changes.

hk1510 added 2 commits June 11, 2026 19:02
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
…modal_hybrid_real.yaml

Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
@hk1510
hk1510 requested review from a team as code owners June 11, 2026 19:11
@hk1510
hk1510 requested review from abhinavg4 and removed request for a team June 11, 2026 19:11
@copy-pr-bot

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

@arhamm1
arhamm1 requested review from suiyoubi and removed request for abhinavg4 June 11, 2026 19:13
@greptile-apps

greptile-apps Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 2/5

[Medium impact] The change reaches a new, operator-run multimodal curation workflow.

Fix the parallel startup failure, preserve shortened text in exports, and bound recall memory use before merging.

Findings

  1. P1 Parallel steps cannot start ▶
  2. P1 Export loses shortened text ▶
  3. P1 Recall runs out of memory ▶
  4. P2 Benchmark config is missing ▶

Summary

Adds an Omni-Fuse tutorial with SNS, expert embeddings, projection training, and query-based dataset selection.

  • SNS trims paired records against their annotations.
  • Each pair gets embeddings from text, fusion, and end-to-end experts.
  • Projection maps expert embeddings into one space for retrieval.
  • A query ranks projected records into a smaller multimodal data blend.
  • SNS and EEE process ordered shards in an overlapping stream.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Paired records] --> B[SNS]
  B --> C[Expert embeddings]
  C --> D[Projection training]
  D --> E[Query ranking]
  E --> F[Ranked and selected records]
Loading

Reviews (11) · Last reviewed commit: "Merge branch 'NVIDIA-NeMo:main' into mai..." · Reviewed by Greptile

Comment on lines +1 to +69
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Small compatibility layer around NeMo Curator APIs."""

from __future__ import annotations

import copy
from typing import Any

import pandas as pd
from nemo_curator.pipeline import Pipeline as CuratorPipeline
from nemo_curator.tasks import DocumentBatch, EmptyTask


def records_from_task(task: Any) -> list[dict[str, Any]]:
"""Return task data as records from a Curator task."""

data = task.data
if hasattr(data, "to_dict"):
try:
return [dict(row) for row in data.to_dict(orient="records")]
except TypeError:
return [dict(row) for row in data.to_dict("records")]
if isinstance(data, list):
return [dict(row) for row in data]
raise TypeError(f"Unsupported task data type: {type(data)!r}")


def make_document_batch(
task_id: str,
dataset_name: str,
records: list[dict[str, Any]],
metadata: dict[str, Any] | None = None,
stage_perf: list[Any] | None = None,
) -> DocumentBatch:
"""Construct a NeMo Curator DocumentBatch."""

return DocumentBatch(
task_id=task_id,
dataset_name=dataset_name,
data=pd.DataFrame.from_records(records),
_metadata=metadata or {},
_stage_perf=stage_perf or [],
)


def make_empty_task() -> EmptyTask:
if callable(EmptyTask):
try:
return EmptyTask(task_id="empty", dataset_name="omnifuse", data=None)
except TypeError:
return EmptyTask()
return copy.deepcopy(EmptyTask)


def make_curator_pipeline(name: str, stages: list[Any], description: str | None = None) -> CuratorPipeline:
return CuratorPipeline(name=name, description=description, stages=stages)

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.

P0 Missing ProcessingStage, Resources, and re-export of EmptyTask

Every stage file (stages/eee.py, stages/sns.py, stages/projection.py, stages/datablend.py, stages/reader.py) imports ProcessingStage and Resources from this module. Neither is defined here or re-exported from it. stages/reader.py also imports EmptyTask directly. All five stage modules will fail immediately with ImportError when loaded, making the entire pipeline non-runnable.

These classes need to be either imported from nemo_curator and re-exported, or implemented as shim classes within this compat module.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed imports in b8fc71b


from omnifuse_tutorial.compat.curator import make_curator_pipeline, make_empty_task
from omnifuse_tutorial.config.models import ExperimentConfig
from omnifuse_tutorial.data.io import write_json

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.

P0 Missing omnifuse_tutorial/data/ package

omnifuse_tutorial/data/io.py and omnifuse_tutorial/data/loader.py are imported by at least eight files — pipeline.py, utils.py, stages/eee.py, stages/projection.py, stages/datablend.py, stages/reader.py, sns/backends.py, and projection/trainer.py — but the entire omnifuse_tutorial/data/ directory does not exist in the repository. Every entry point to the tutorial will fail at import time with ModuleNotFoundError: No module named 'omnifuse_tutorial.data'.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added in recent commit. Was .gitignored previously.

Comment on lines +492 to +498
def _audio_format(path: Path) -> str:
audio_format = path.suffix.lower().lstrip(".")
if audio_format == "mpeg":
audio_format = "mp3"
if audio_format not in {"wav", "mp3"}:
raise ValueError(f"NVIDIA audio descriptions support wav/mp3 only, got {path.suffix} for {path}")
return audio_format

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 _audio_format raises for extensions declared as supported

AUDIO_EXTENSIONS and MIME_TYPES both include .flac, .m4a, .aac, and .ogg, advertising them as valid inputs. But _audio_format raises ValueError for any extension other than .wav and .mp3. The API description path in NvidiaApiEEEBackend._describe_raw → _describe_file → describe_file_with_nvidia_api calls this function unconditionally for audio records, so a user who provides a .flac or .m4a file will receive a confusing ValueError rather than a clear unsupported-format message.

Reconcile by either removing the unsupported extensions from AUDIO_EXTENSIONS/MIME_TYPES, converting to a supported format before sending, or raising a clear error with guidance early in the audio code path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b8fc71b

"pandas>=2.0",
"pyyaml>=6.0",
"requests>=2.31",
"spacy>=3.8.14",

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 Duplicate spacy constraint — the same package appears twice with conflicting lower bounds (>=3.8.14 on line 17 and >=3.8,<4 on line 35). Keep a single, unambiguous entry.

Suggested change
"spacy>=3.8.14",
"spacy>=3.8.14,<4",

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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 367d7e9

"qwen-vl-utils>=0.0.8",
"sentence-transformers>=2.2,<4",
"soundfile>=0.12",
"spacy>=3.8,<4",

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 Remove the second duplicate spacy entry.

Suggested change
"spacy>=3.8,<4",
# spacy consolidated above as spacy>=3.8.14,<4

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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 367d7e9

path = Path(value)
except OSError:
return None
return path if path.exists() else path

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 _path_or_none always returns a Path, never None — return path if path.exists() else path is a tautology; both branches return the same value. The | None return type annotation is misleading. Either return None when the path does not exist, or simplify to return path.

Suggested change
return path if path.exists() else path
return path

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 367d7e9

Comment on lines +20 to +22

```bash
uv sync --extra dev

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 Incorrect setup directory in README

The instruction says to run uv sync --extra dev "from the Curator repository root", but uv sync reads pyproject.toml from the current directory. No uv workspace configuration ties this tutorial to the repository root, so running uv sync from the repo root installs the main Curator package rather than this tutorial's dependencies. Users should cd tutorials/multimodal/omni-fuse-data-curation/ first.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 367d7e9

hk1510 and others added 4 commits June 11, 2026 20:34
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Comment on lines +50 to +56
return DocumentBatch(
task_id=task_id,
dataset_name=dataset_name,
data=pd.DataFrame.from_records(records),
_metadata=metadata or {},
_stage_perf=stage_perf or [],
)

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 task_id is declared as field(init=False, default="") in the Task base class, so it is intentionally excluded from DocumentBatch.__init__. Passing task_id=task_id here raises TypeError: __init__() got an unexpected keyword argument 'task_id' at runtime. Because every stage calls make_document_batch, the entire pipeline fails before any data is processed. The task_id is managed by the framework (set via _set_task_id() at each stage boundary) and must not be supplied to the constructor.

Suggested change
return DocumentBatch(
task_id=task_id,
dataset_name=dataset_name,
data=pd.DataFrame.from_records(records),
_metadata=metadata or {},
_stage_perf=stage_perf or [],
)
return DocumentBatch(
dataset_name=dataset_name,
data=pd.DataFrame.from_records(records),
_metadata=metadata or {},
_stage_perf=stage_perf or [],
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 21b8091

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Reverted 21b8091 as DocumentBatch requires task_id

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.

You're right to revert. My original comment was incorrect — task_id is defined as field(init=False, default="") on the Task base class, which means Python's @dataclass machinery excludes it from __init__ entirely. Passing task_id=task_id will indeed raise TypeError: __init__() got an unexpected keyword argument 'task_id' at runtime.

The current code in the PR still has this bug. The fix from commit 21b8091 (which you reverted) was correct — task_id should be omitted from the DocumentBatch(...) constructor call and set afterwards via _set_task_id() if needed, or simply left to the framework to assign.

Here's the correct version:

Suggested change
return DocumentBatch(
task_id=task_id,
dataset_name=dataset_name,
data=pd.DataFrame.from_records(records),
_metadata=metadata or {},
_stage_perf=stage_perf or [],
)
return DocumentBatch(
dataset_name=dataset_name,
data=pd.DataFrame.from_records(records),
_metadata=metadata or {},
_stage_perf=stage_perf or [],
)

If the caller needs to set a specific task_id, it should be done after construction:

batch = make_document_batch(...)
batch._set_task_id("", task_id)  # or however it fits the pipeline DAG

But in most cases, the framework handles task_id assignment automatically — make_document_batch doesn't need the task_id parameter at all.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e5d3594

Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Comment on lines +115 to +124
task = run_curator_step(
name=f"{config.experiment_id}-2-embed",
stages=[EEEEmbeddingStage(config=config)],
initial_task=load_sns_task(config),
)
records_path = config.run_dir / "embeddings" / "records.jsonl"
write_jsonl(records_path, records_from_task(task))
metadata = dict(getattr(task, "_metadata", {}) or {})
metadata["embedding_records_path"] = str(records_path)
task._metadata = metadata

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 task_id is not a parameter of make_document_batch

load_sns_task, load_embedding_task, and load_projection_task all call make_document_batch(task_id=..., dataset_name=..., records=..., metadata=...), but the current make_document_batch signature in compat/curator.py is (dataset_name, records, metadata=None, stage_perf=None) — it has no task_id parameter. Every one of these three loaders will raise TypeError: make_document_batch() got an unexpected keyword argument 'task_id' as soon as it is called, breaking all three resume paths (2_embed.py, 3_project.py, 4_datablend.py). Remove the task_id= keyword from all three call sites.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Reverted 21b8091 in 59225f5 so this should no longer be an issue.

DocumentBatch requires task_id

This reverts commit 21b8091.

Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
hk1510 added 2 commits June 16, 2026 01:52
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Comment on lines +57 to +68
selected = ranker.select_top(ranked)

output_dir = self.config.run_dir / "datablend"
ranked_path = write_jsonl(output_dir / "datablend_ranked.jsonl", ranked)
topk_path = write_jsonl(output_dir / "datablend_topk.jsonl", selected)
metadata.update(
{
"datablend_ranked_path": str(ranked_path),
"datablend_topk_path": str(topk_path),
"datablend_size": len(selected),
}
)

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 datablend_size metadata contradicts the batch record count

make_document_batch is called with records=ranked (all N ranked records), but datablend_size in metadata is set to len(selected) (the top-k count). Any downstream code that consumes the output task via records_from_task(task) receives all N records, not datablend_size records — the two values will silently disagree. If the intent is for the curated output batch to contain only the selected subset, records=selected should be passed instead. If the intent is to carry all rankings forward, datablend_size should reflect len(ranked) (or be renamed to datablend_topk_size) so the contract is unambiguous.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in eabec7d

Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Comment on lines +267 to +292
def _describe_chat_completion_file(
path: Path,
model: str,
content_type: str,
prompt: str,
api_base_url: str,
headers: dict[str, str],
timeout: int,
) -> str:
encoded = base64.b64encode(path.read_bytes()).decode("utf-8")
mime = MIME_TYPES.get(path.suffix.lower(), "application/octet-stream")
if content_type == "input_audio":
media_content = {"type": "input_audio", "input_audio": {"data": encoded, "format": _audio_format(path)}}
else:
media_content = {content_type: {"url": f"data:{mime};base64,{encoded}"}, "type": content_type}
url = f"{api_base_url}/chat/completions"
payload = {
"model": model,
"messages": [{"role": "user", "content": [{"type": "text", "text": prompt}, media_content]}],
"max_tokens": 512,
"temperature": 0.2,
"stream": False,
}
response = _post_nvidia_json_with_retries(url, headers, payload, timeout)
response = _resolve_nvidia_response(response, api_base_url, headers, timeout, model, url)
return _response_text(response.json(), model, url)

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 Video/image files base64-encoded inline with no size guard

_describe_chat_completion_file reads the entire file with path.read_bytes() and embeds it as a base64 data-URL in the JSON body for both image_url and video_url content types. The audio code path has an explicit NVCF_ASSET_UPLOAD_THRESHOLD_BYTES = 180 * 1024 check and falls back to NVCF asset upload for large files. No equivalent guard exists here. A typical 10-second video file is easily 10–50 MB before encoding; base64 adds ~33 %, producing a JSON body the NVIDIA API will reject with HTTP 413 or a generic error — the same limit that drove the audio threshold. This silently breaks the video modality for any non-trivial input. Consider adding a size check and routing large image/video files through _upload_nvcf_asset (or raising an early ValueError with a clear message) just as audio already does.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 2b13f6f

hk1510 added 2 commits June 16, 2026 03:43
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
Signed-off-by: Harshil Kotamreddy <hkotamreddy@nvidia.com>
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-maintainers Waiting on maintainers to respond label Aug 6, 2026
@VibhuJawa

Copy link
Copy Markdown
Contributor

/review model=codex

@sarahyurick

Copy link
Copy Markdown
Contributor

/review model=codex

@svcnvidia-nemo-ci svcnvidia-nemo-ci 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.

What this change does: Adds a standalone tutorial that refines multimodal pairs, builds expert embeddings, trains a projection, and exports query-ranked records. It also adds a sharded SNS-to-EEE pipeline for overlapping work across GPUs.

Reviewed all 47 changed files and traced their Curator contracts and artifact consumers. Assumed one tutorial user, using either the numbered scripts or the documented two-GPU profile.

Must fix

Findings 1–4 below cover package installation, query embedding selection, exported text, and disabled-stage validation.

Should fix

Findings 5–6 cover regression tests and an unavailable documented configuration.

The current source addresses the earlier missing imports/data package, constructor compatibility, selected-record count, and large-image/video upload findings. Those are not repeated here.

Verdict: fix 1–4 first.

Not checked: dependency installation, tests, hosted endpoints, or GPU execution. All patches and acceptance checks below are proposed, not executed; verify them before adopting.

Note: formal approval is not available for this request, so this review is posted as a comment only.

Review generated by codex. This was a full review. Request a full review with /review reset=true.

en-core-web-sm = { url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" }

[tool.setuptools]
py-modules = []

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.

  1. Must fix: Include the tutorial package in the installation
  • What this is: The build configuration explicitly sets py-modules = [] but never selects any packages.
  • Problem: An explicit module list disables setuptools' automatic discovery, so the built distribution contains no omnifuse_tutorial package. Importing the installed tutorial outside its source directory therefore fails with ModuleNotFoundError; running scripts from the checkout can hide this packaging failure.
  • Fix: Keep the empty top-level module list and add this package selection immediately afterward:
    [tool.setuptools.packages.find]
    include = ["omnifuse_tutorial", "omnifuse_tutorial.*"]
    This must install every tutorial subpackage without packaging datasets, model checkouts, or numbered scripts. Proposed acceptance: inspect the built wheel for the package and its subpackages, then install it with dependencies and run import omnifuse_tutorial from a temporary directory with no checkout on PYTHONPATH; retain successful imports when running the documented scripts from the tutorial directory.
  • If we skip it: The distribution installs successfully but does not provide its implementation.

backend: EEEBackend

def rank(self, records: list[dict[str, Any]], projection: ProjectionResult) -> list[dict[str, Any]]:
query_embedding = self.backend.embed_query(self.config.query, expert="text-based")

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.

  1. Must fix: Embed the query with the projection's anchor expert
  • What this is: Both projection trainers choose an anchor_expert, falling back to the first configured expert when text-based is absent.
  • Problem: With a supported nonempty expert list such as ["fusion"], the torch projection targets LanguageBind annotation embeddings, but ranking always compares its output with a Nemotron text-based query. Equal vector lengths conceal that these are different embedding spaces, producing unrelated similarity scores rather than ranking against the trained target space.
  • Fix: In DatablendRanker.rank, replace the hardcoded selection with:
    anchor_expert = projection.model["anchor_expert"]
    query_embedding = self.backend.embed_query(self.config.query, expert=anchor_expert)
    Both trainers write this field, and the resume loader preserves it. Proposed acceptance: use a fake backend whose text-based query is [1, 0] and fusion query is [0, 1]; with anchor_expert="fusion" and projected rows [[0, 1], [1, 0]], the first record must rank first, including after artifact reload; retain text-based query selection for the default expert list and use e2e for an e2e-only anchor.
  • If we skip it: Removing the text-based expert silently invalidates the final ranking.

"pair_id": record["pair_id"],
"pool": record["pool"],
"modality": record["modality"],
"raw_path": record["raw_path"],

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.

  1. Must fix: Preserve the refined raw text in the exported records
  • What this is: Text forward extraction stores its retained sentences in sns_raw_text, while leaving raw_path pointed at the original file.
  • Problem: If the original file contains A cat. A truck. and SNS retains only A cat., EEE embeds the shorter text, but this export drops both text fields and retains only the original file path. The resulting datablend cannot recover the refined sample that was ranked.
  • Fix: Before appending each row, preserve the effective text explicitly:
    if record["modality"] == "text":
        row["raw_text"] = record.get("sns_raw_text") or record.get("raw_text")
    Document this exported field as the effective text value; retain raw_path for provenance and keep media rows pointing to their actual media files, not descriptions. Proposed acceptance: assert that the example exports raw_text == "A cat."; disabled or rejected text extraction must export the original text, and image/audio/video rows must retain their existing paths and annotations; verify these content guarantees with include_metadata both true and false.
  • If we skip it: Text curation is lost at the final export even though the embeddings used the curated content.

"bidirectional",
}
if (
effective_sns_backend in {"hybrid", "local"}

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.

  1. Must fix: Do not require a video extraction checkpoint when SNS is disabled
  • What this is: Input validation treats any video pool with a forward direction as requiring CG-DETR.
  • Problem: Set sns.enabled: false in the supplied hybrid config and provide valid data, an API key, and LanguageBind, but no CG-DETR checkpoint: Step 0 still raises a missing-assets error. The disabled processor branch returns the original pair without ever using that checkpoint, so validation rejects a runnable configuration.
  • Fix: Add config.sns.enabled to this checkpoint requirement:
    if (
        config.sns.enabled
        and effective_sns_backend in {"hybrid", "local"}
        and uses_video_forward
        and not config.sns.cg_detr_checkpoint.exists()
    ):
    Proposed acceptance: with one valid video mapping and the other required assets available, disabled SNS plus a missing checkpoint must pass validation; enabled hybrid/local forward or bidirectional SNS must still reject the missing checkpoint, while backward-only and API SNS must remain exempt.
  • If we skip it: Users cannot use the advertised disable switch to bypass local video extraction setup.

return summary


def merge_parallel_outputs(config: ExperimentConfig, output_tasks: list[Any]) -> dict[str, Any]:

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.

  1. Should fix: Add a CPU regression test for shard merging
  • What this is: The new merge path sorts completed tasks, combines manifests, and reconstructs alternating raw/annotation arrays.
  • Problem: This PR adds no test for those contracts. A change to sorting or alternating-row slicing can still produce valid-shaped files while assigning one record another record's embedding.
  • Fix: Add a CPU-only test using fake SNS/EEE backends and temporary artifact directories; the invariant is that record order, manifest order, and each expert's raw/annotation rows stay aligned. Proposed acceptance: shard three records a,b,c with size two, return completed shards in reverse order, and assert canonical record/manifest IDs are a,b,c; with raw vectors [i,0] and annotation vectors [0,i] for i=1,2,3, assert the merged raw and annotation arrays preserve those exact sequences; retain rejection of duplicate/missing shards and incorrect array dimensions, including the one-record final shard.
  • If we skip it: The most important parallel-output contract remains unprotected without running expensive models.

```

For the five-dataset benchmark used in this tutorial, the matching two-GPU,
4,000-epoch config is `configs/omni_fuse_1000_4000ep_parallel.yaml`.

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.

  1. Should fix: Remove the unavailable benchmark configuration reference
  • What this is: The benchmark instructions name a five-dataset, 4,000-epoch configuration.
  • Problem: That file is absent from the head tree; the configuration directory contains only the hybrid tutorial YAML. Passing the named benchmark profile to a tutorial command therefore fails while loading the configuration.
  • Fix: Delete these two benchmark-reference lines until that profile is included, retaining the existing parallel command and tuning examples. Proposed acceptance: inspect each configuration filename named by the README and confirm that it exists; the default sequential and parallel examples must still name the supplied hybrid configuration.
  • If we skip it: Readers are directed to a benchmark setup they cannot reproduce from this checkout.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added the waiting-on-customer Waiting on the original author to respond label Oct 22, 2026
Comment on lines +52 to +53
if self.worker_count is not None:
spec["num_workers"] = self.worker_count

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 Parallel steps cannot start

The parallel path always sets worker_count, so SNSStage.xenna_stage_spec() returns num_workers. The current XennaExecutor.execute() rejects that key with ValueError before starting workers. EEEEmbeddingStage does the same thing.

Override num_workers() to return worker_count in both stages, and keep only slots_per_actor in xenna_stage_spec().

"pair_id": record["pair_id"],
"pool": record["pool"],
"modality": record["modality"],
"raw_path": record["raw_path"],

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 Export loses shortened text

For text records, forward SNS saves the shortened text only in sns_raw_text; raw_path still points to the original file. DatablendRanker.rank() exports that original path but drops sns_raw_text. Users loading the final datablend get the unfiltered text, even though the embeddings and ranking used the shortened text.

Include the refined text in the export, or write it to a new file and export that path.

Suggested change
"raw_path": record["raw_path"],
"raw_path": record["raw_path"],
"sns_raw_text": record.get("sns_raw_text"),

Comment on lines +325 to +328
similarities = annotations @ projected.T
targets = torch.arange(similarities.shape[0], device=similarities.device).unsqueeze(1)
annotation_to_raw = (similarities.topk(k, dim=1).indices == targets).any(dim=1).float().mean()
raw_to_annotation = (similarities.T.topk(k, dim=1).indices == targets).any(dim=1).float().mean()

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 Recall runs out of memory

_torch_recall_at_k() builds an all-record similarity matrix after training. Its memory use grows with the square of the record count, regardless of projection.batch_size. At 150,000 records, this float32 matrix alone needs about 90 GB. Projection therefore cannot finish on the documented 46 GiB GPUs even if training batches fit, and it fails before saving the model or projected embeddings.

Compute recall in chunks. The final model forward should also respect the configured batch size.

Comment on lines +284 to +285
For the five-dataset benchmark used in this tutorial, the matching two-GPU,
4,000-epoch config is `configs/omni_fuse_1000_4000ep_parallel.yaml`.

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 Benchmark config is missing

The README points users to configs/omni_fuse_1000_4000ep_parallel.yaml, but that file is not included. Users cannot run the described benchmark with the supplied files. Add the config or remove the reference.

Suggested change
For the five-dataset benchmark used in this tutorial, the matching two-GPU,
4,000-epoch config is `configs/omni_fuse_1000_4000ep_parallel.yaml`.

@greptile-apps

greptile-apps Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

This branch was successfully deployed

3 active (outdated) deployments
nemo-ci — d37ecf08 Deployed Oct 22, 2026 by copy-pr-bot[bot] via L0_Unit_Test_GPU-video #5903
public — d37ecf08 Deployed Oct 22, 2026 by copy-pr-bot[bot] via release / finalize / notify #1425
test — d37ecf08 Deployed Oct 22, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #5903
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-request waiting-on-customer Waiting on the original author to respond

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants