[None][feat] Exact multimodal KV blockhashing#13815
Conversation
📝 WalkthroughWalkthroughThis PR extends TensorRT-LLM with multimodal run metadata support, adding optional fields describing flat token run layouts (per-item offsets, positions, lengths) to enable run-based cache key generation. The implementation includes backward-compatible serialization, refactored block-key extra-key generation supporting both run-based and legacy paths, Python validation and run derivation helpers, and comprehensive test coverage across C++ and Python layers. ChangesMultimodal Run Metadata Support
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
tests/unittest/llmapi/test_llm_kv_cache_events.py (1)
412-458: Strong unit coverage for exact-run buffer validation and int32 limits.This adds both functional and failure-mode checks for the new multimodal run metadata, including overflow boundary behavior.
QA list updates are unnecessary here since the PR changes in this file are unit-test scoped (no new/changed
tests/integration/defs/...coverage to schedule).Also applies to: 460-482
🤖 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/unittest/llmapi/test_llm_kv_cache_events.py` around lines 412 - 458, Add the same exact-run buffer validation and int32 overflow boundary tests for multimodal run metadata in the sibling test block (lines referenced 460-482): use _find_mm_token_runs_from_mask to compute item_run_cu_offsets/run_positions/run_lengths, assert those values, create MultimodalInput via from_components and assert the stored multimodal_* attributes, then add the two failure-mode assertions that instantiate MultimodalInput with mismatched provided run metadata and with a run length that overflows (expecting ValueError with the same "must be provided together" and "sum to 3, expected 2" messages); reference the functions/classes MultimodalInput, MultimodalInput.from_components, and _find_mm_token_runs_from_mask to locate where to add the duplicated checks.cpp/tensorrt_llm/executor/serialization.cpp (1)
392-431: 💤 Low valueLGTM — serialize/serializedSize are mutually consistent.
serializeandserializedSizeproduce a coherent byte layout. One minor inconsistency worth noting:serializecacheshasMultimodalInputRunExtension(multimodalInput)in aconstlocal (line 394), whileserializedSizeevaluates it twice (lines 415 and 424). Storing it in a local variable would harmonise the two functions.♻️ Suggested consistency improvement
size_t Serialization::serializedSize(MultimodalInput const& multimodalInput) { size_t totalSize = 0; + auto const hasRunExtension = hasMultimodalInputRunExtension(multimodalInput); - if (hasMultimodalInputRunExtension(multimodalInput)) + if (hasRunExtension) { totalSize += su::serializedSize(kMultimodalInputRunExtensionTag); totalSize += su::serializedSize(kMultimodalInputRunExtensionVersion); } totalSize += su::serializedSize(multimodalInput.mMultimodalHashes); totalSize += su::serializedSize(multimodalInput.mMultimodalPositions); totalSize += su::serializedSize(multimodalInput.mMultimodalLengths); totalSize += su::serializedSize(multimodalInput.mMultimodalUuids); - if (hasMultimodalInputRunExtension(multimodalInput)) + if (hasRunExtension) {🤖 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 `@cpp/tensorrt_llm/executor/serialization.cpp` around lines 392 - 431, serializedSize recomputes hasMultimodalInputRunExtension(multimodalInput) twice; to match serialize's approach, call hasMultimodalInputRunExtension once and cache the result in a local const (e.g. hasRunExtension) at the top of Serialization::serializedSize, then use that variable in both conditional blocks when adding kMultimodalInputRunExtensionTag, kMultimodalInputRunExtensionVersion and the multimodal run fields (mMultimodalItemRunCuOffsets, mMultimodalRunPositions, mMultimodalRunLengths).tests/unittest/_torch/executor/test_kv_cache_v2_multimodal_runs.py (1)
28-78: ⚡ Quick winAdd a two-item exact-run case here.
These exact-run assertions only cover a single multimodal item, so they never exercise the new
run_item_indices/run_item_offsetsmapping across multiple items. A small two-item fixture would catch regressions in the per-item offset math much earlier.As per coding guidelines, "Assess whether new/changed tests cover happy path, important edge cases, and failure modes relevant to the feature or fix."
🤖 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/unittest/_torch/executor/test_kv_cache_v2_multimodal_runs.py` around lines 28 - 78, Add a new unit test that exercises a two-item multimodal-run case to validate per-item offset math in KVCacheManagerV2._augment_tokens_for_block_reuse: create a fixture with two multimodal items (e.g., multimodal_hashes containing two hashes, multimodal_positions, multimodal_lengths, multimodal_item_run_cu_offsets containing offsets for both items, and multimodal_run_positions/run_lengths covering runs that span both items), generate expected mm_tokens for each item via gen_multimodal_cache_key_tokens, call _augment_tokens_for_block_reuse with full and sliced ranges, and assert that tokens are replaced at the correct indices for each item (and that runs outside the slice are skipped); this will exercise the run_item_indices / run_item_offsets mapping across multiple items.tensorrt_llm/_torch/pyexecutor/resource_manager.py (1)
164-183: ⚡ Quick winMaterialize the overlap tensors before the Python loop.
This runs on the pyexecutor block-reuse path, but the loop still does tensor masking/indexing per multimodal item. Converting the overlap metadata to Python lists once before iterating will avoid repeated tensor-bound work in a hot path.
Based on learnings: In files under
tensorrt_llm/_torch/pyexecutor, avoid accessingtorch.Tensorobjects inside for-loops when iterating over requests. Convert batched tensors to Python lists beforehand usingtensor.tolist(), and then iterate over those lists.🤖 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 `@tensorrt_llm/_torch/pyexecutor/resource_manager.py` around lines 164 - 183, The loop is repeatedly indexing and operating on torch.Tensors (overlap_run_item_indices, overlap_item_indices, overlap_starts, overlap_ends, token_offsets, result_offsets, lengths, etc.), which is expensive on the hot pyexecutor path; before the Python for-loop that iterates multimodal items (the block around overlap_item_indices and the for result_offset,... zip(..., strict=True)), convert the per-item tensors to native Python lists (e.g., use .tolist() or .cpu().tolist()) and materialize any small per-item arrays used inside the loop (e.g., overlap_run_item_indices, overlap_starts, overlap_ends, token_offsets, result_offsets, lengths, and multimodal_hashes entries used by _hash_to_digest) so the inner loop uses only Python scalars/lists rather than indexing torch.Tensors. Ensure you still compute digest = _hash_to_digest(...) with the materialized multimodal hash value, and preserve the existing semantics and ordering.
🤖 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 `@cpp/tensorrt_llm/batch_manager/blockKey.cpp`:
- Around line 211-215: The overlap check uses signed addition startPos + length
which can overflow; change the legacy branch to compute a safe run end using
getValidRunEnd(startPos, length) and use that value in the if-condition and when
computing mmStartInBlock so the logic mirrors the run-based path; update the
condition if (endTokenIdx > startPos && startTokenIdx < startPos + length) to
use the safe end (e.g. auto const runEnd = getValidRunEnd(startPos, length); if
(endTokenIdx > startPos && startTokenIdx < runEnd) { auto const mmStartInBlock =
startPos >= startTokenIdx ? 0 : startTokenIdx - startPos; ... }) ensuring you
reference makeHashArray(multimodalHashes[itemIdx]) and getUuid(multimodalUuids,
itemIdx) unchanged when emplacing into extraKeys.
In `@tensorrt_llm/_torch/pyexecutor/resource_manager.py`:
- Around line 100-105: Replace the debug-only assert in _hash_to_digest with
explicit runtime validation that raises a ValueError when the input is
malformed: check that hash_ints is a Sequence of length exactly 8 (and
optionally that each element is an int) and raise ValueError with a clear
message if not, before converting values to bytes; keep the existing conversion
logic using v.to_bytes('big', signed=True) unchanged so the function still
returns the 32-byte digest when validation passes.
In `@tensorrt_llm/inputs/multimodal.py`:
- Line 149: Before calling _validate_multimodal_runs(), add an explicit
cardinality check that len(self.multimodal_hashes) ==
len(self.multimodal_positions) == len(self.multimodal_lengths) and raise a clear
ValueError if not; this prevents an IndexError later when building
multimodal_item_run_cu_offsets (and the per-item loop indexed by
self.multimodal_lengths) — apply the same pre-check in both places where
_validate_multimodal_runs() is invoked so the three multimodal lists are
guaranteed aligned before further processing.
---
Nitpick comments:
In `@cpp/tensorrt_llm/executor/serialization.cpp`:
- Around line 392-431: serializedSize recomputes
hasMultimodalInputRunExtension(multimodalInput) twice; to match serialize's
approach, call hasMultimodalInputRunExtension once and cache the result in a
local const (e.g. hasRunExtension) at the top of Serialization::serializedSize,
then use that variable in both conditional blocks when adding
kMultimodalInputRunExtensionTag, kMultimodalInputRunExtensionVersion and the
multimodal run fields (mMultimodalItemRunCuOffsets, mMultimodalRunPositions,
mMultimodalRunLengths).
In `@tensorrt_llm/_torch/pyexecutor/resource_manager.py`:
- Around line 164-183: The loop is repeatedly indexing and operating on
torch.Tensors (overlap_run_item_indices, overlap_item_indices, overlap_starts,
overlap_ends, token_offsets, result_offsets, lengths, etc.), which is expensive
on the hot pyexecutor path; before the Python for-loop that iterates multimodal
items (the block around overlap_item_indices and the for result_offset,...
zip(..., strict=True)), convert the per-item tensors to native Python lists
(e.g., use .tolist() or .cpu().tolist()) and materialize any small per-item
arrays used inside the loop (e.g., overlap_run_item_indices, overlap_starts,
overlap_ends, token_offsets, result_offsets, lengths, and multimodal_hashes
entries used by _hash_to_digest) so the inner loop uses only Python
scalars/lists rather than indexing torch.Tensors. Ensure you still compute
digest = _hash_to_digest(...) with the materialized multimodal hash value, and
preserve the existing semantics and ordering.
In `@tests/unittest/_torch/executor/test_kv_cache_v2_multimodal_runs.py`:
- Around line 28-78: Add a new unit test that exercises a two-item
multimodal-run case to validate per-item offset math in
KVCacheManagerV2._augment_tokens_for_block_reuse: create a fixture with two
multimodal items (e.g., multimodal_hashes containing two hashes,
multimodal_positions, multimodal_lengths, multimodal_item_run_cu_offsets
containing offsets for both items, and multimodal_run_positions/run_lengths
covering runs that span both items), generate expected mm_tokens for each item
via gen_multimodal_cache_key_tokens, call _augment_tokens_for_block_reuse with
full and sliced ranges, and assert that tokens are replaced at the correct
indices for each item (and that runs outside the slice are skipped); this will
exercise the run_item_indices / run_item_offsets mapping across multiple items.
In `@tests/unittest/llmapi/test_llm_kv_cache_events.py`:
- Around line 412-458: Add the same exact-run buffer validation and int32
overflow boundary tests for multimodal run metadata in the sibling test block
(lines referenced 460-482): use _find_mm_token_runs_from_mask to compute
item_run_cu_offsets/run_positions/run_lengths, assert those values, create
MultimodalInput via from_components and assert the stored multimodal_*
attributes, then add the two failure-mode assertions that instantiate
MultimodalInput with mismatched provided run metadata and with a run length that
overflows (expecting ValueError with the same "must be provided together" and
"sum to 3, expected 2" messages); reference the functions/classes
MultimodalInput, MultimodalInput.from_components, and
_find_mm_token_runs_from_mask to locate where to add the duplicated checks.
🪄 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: 1e8117b1-fdce-4914-93db-84eeb3df59be
📒 Files selected for processing (23)
cpp/include/tensorrt_llm/batch_manager/llmRequest.hcpp/include/tensorrt_llm/executor/executor.hcpp/tensorrt_llm/batch_manager/blockKey.cppcpp/tensorrt_llm/executor/multimodalInput.cppcpp/tensorrt_llm/executor/serialization.cppcpp/tensorrt_llm/nanobind/batch_manager/bindings.cppcpp/tensorrt_llm/nanobind/batch_manager/llmRequest.cppcpp/tensorrt_llm/nanobind/batch_manager/llmRequest.hcpp/tensorrt_llm/nanobind/executor/request.cppcpp/tests/unit_tests/batch_manager/blockKeyTest.cppcpp/tests/unit_tests/executor/serializeUtilsTest.cpptensorrt_llm/_torch/pyexecutor/_util.pytensorrt_llm/_torch/pyexecutor/llm_request.pytensorrt_llm/_torch/pyexecutor/resource_manager.pytensorrt_llm/executor/base_worker.pytensorrt_llm/inputs/multimodal.pytensorrt_llm/inputs/registry.pytensorrt_llm/runtime/kv_cache_manager_v2/__init__.pytensorrt_llm/runtime/kv_cache_manager_v2/__init__.pyitensorrt_llm/runtime/kv_cache_manager_v2/_block_radix_tree.pytests/unittest/_torch/executor/test_kv_cache_v2_multimodal_runs.pytests/unittest/bindings/test_executor_bindings.pytests/unittest/llmapi/test_llm_kv_cache_events.py
77f9e0c to
2428815
Compare
|
/bot run |
|
PR_Github #47792 [ run ] triggered by Bot. Commit: |
SimengLiu-nv
left a comment
There was a problem hiding this comment.
When any exact-run field is present but the run metadata is incomplete or malformed, this returns an empty extra-key vector. That does not disable KV reuse; it builds ordinary text/placeholder-only BlockKeys, so two requests with the same prompt placeholder IDs but different multimodal content can collide and reuse the wrong KV. Since the legacy multimodal hashes/positions/lengths are still available, this path should either fall back to the legacy extra-key generation or make the request uncacheable instead of returning reusable empty keys.
Assume two requests differ only by image content.
Setup
tokens_per_block = 4- Prompt tokens for both requests:
[101, IMG, IMG, 102, 103] IMGis the same placeholder token ID in both requests.- Request A image hash:
hashA - Request B image hash:
hashB - Legacy metadata is present:
multimodal_hashes = [hashA/hashB]multimodal_positions = [1]multimodal_lengths = [2]
- Exact-run metadata is malformed:
multimodal_item_run_cu_offsets = [0, 1]multimodal_run_positions = nullmultimodal_run_lengths = null
What Happens Today
generateBlockHashExtraKeys()sees at least one exact-run field, so it callsresolveRunMetadata().resolveRunMetadata()returnskInvalidbecause the three run fields are not all present.- The current code returns
{}fromblockKey.cpp. buildBlockKeys()still creates reusableBlockKeys withextraKeys = {}.- For block
[101, IMG, IMG, 102], Request A and Request B now have identical block keys:- same token IDs:
[101, IMG, IMG, 102] - same
loraTaskId - same
cacheSaltID - same
extraKeys = {}
- same token IDs:
- The KV cache can reuse Request A’s cached block for Request B, even though
hashA != hashB.
Why This Is Wrong
extraKeys is the only thing that makes identical multimodal placeholder tokens distinguish different media content. Returning an empty vector does not mean “disable reuse”; it means “hash this like ordinary text.” For multimodal placeholders, that is unsafe.
Safe Behavior
Preferred behavior:
- If exact-run metadata is invalid but legacy
multimodal_positions,multimodal_lengths, andmultimodal_hashesare valid, fall back togenerateLegacyContiguousExtraKeys(...).
Stricter behavior:
- Mark the request or block as uncacheable instead of producing normal text-only block keys.
Unsafe behavior:
- Returning
{}while still committing/reusing the block.
|
PR_Github #47792 [ run ] completed with state
|
|
/bot run |
|
PR_Github #48032 [ run ] triggered by Bot. Commit: |
moraxu
left a comment
There was a problem hiding this comment.
Thanks for the comments in resource_manager.py
|
PR_Github #48032 [ run ] completed with state
|
55e6a1c to
bad1d41
Compare
|
/bot run |
|
PR_Github #49158 [ run ] completed with state |
b62ac47 to
538536e
Compare
|
/bot run |
|
PR_Github #49204 [ run ] triggered by Bot. Commit: |
|
PR_Github #49193 [ run ] completed with state |
|
PR_Github #49204 [ run ] completed with state
|
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Rename the public exact-run cumulative field to multimodal_item_run_cu_offsets and keep the C++/nanobind/Python surfaces aligned. Reject exact-run end positions above INT32_MAX during Python validation so invalid metadata fails locally before reaching C++. Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Gate exact multimodal run buffers behind a versioned MultimodalInput serialization tag so new readers keep legacy four-field streams aligned. Keep no-run writers on the legacy wire shape and add regressions for legacy stream alignment. Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
538536e to
71e4c3f
Compare
|
/bot run |
|
PR_Github #49249 [ run ] triggered by Bot. Commit: |
|
PR_Github #49249 [ run ] completed with state
|
|
/bot run |
|
PR_Github #49273 [ run ] triggered by Bot. Commit: |
|
PR_Github #49273 [ run ] completed with state |
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Signed-off-by: venkywonka <23023424+venkywonka@users.noreply.github.com>
Description
Today's multimodal KV-cache block hashing assumes each multimodal item occupies one contiguous
[pos, pos+length)span in the prompt. Production VLM preprocessors already break that assumption.Frame N sampled at X.XX seconds:gaps[1026, 2, 2, 2, 2, 2, 2, 2][10, 890)and[898, 1778)separated by "<vision_end><0.2 seconds><vision_start>"What goes wrong under the contiguous claim
The producer emits
multimodal_lengths[i] = num_mm_tokens(a count of mm tokens, not a span width). Both consumers then re-derive a span as[pos, pos + length), which is not true for any item whose patches sit in non-contiguous runs:blockKey.cpp::generateBlockHashExtraKeys) -MmKey.startOffsetis computed prompt-relative (block_start - pos), which only equals the item-local offset for contiguous items. Tail blocks pastpos + lengthreceive noMmKeyat all, even when they hold real mm patches (e.g. positions[2088, 2183)of an 8-frame Nemotron video).KVCacheManagerV2._augment_tokens_for_block_reuse) - overwrites positions inside the claimed span with digest-derived synthetic IDs. Real text tokens that fall in the gaps (Frame 2 sampled at 2.51 seconds:) are erased before hashing. Tail mm positions outside the span keep their placeholder IDs and carry no digest contribution at all.Practical impact - correctness debt, not a hot incident
Surface-level cross-prompt collisions are rare in production today:
What the PR actually closes is structural fragility that might surface as preprocessors evolve:
What this PR adds
Three optional flat arrays alongside the existing fields, so they cross the Python <-> C++ <-> serialization boundary cleanly:
End-to-end plumbing:
tensorrt_llm/inputs/multimodal.py::_find_mm_token_runs_from_maskextracts the runs from the per-token mm mask, called frommultimodal_hashing_processininputs/registry.py.MultimodalInputPython dataclass gains the three fields plus_validate_multimodal_runs(all-or-nothing, monotoniccu,sum(run_lengths(item)) == multimodal_lengths[item], runs ordered + non-overlapping, values fit in int32,pos+len <= INT32_MAX). C++executor::MultimodalInput,serialization.cpp, and the nanobind binding follow;__setstate__accepts both legacy 4-tuple and new 7-tuple state for backward-compatible deserialization.GenericLlmRequestgains 3 optionalshared_ptr<vector<SizeType32>>fields;executor_request_to_llm_requestand the nanobind ctor forward them.resolve + run-based + legacy-fallbackhelpers:generateBlockHashExtraKeys->resolveRunMetadata/generateRunBasedExtraKeys/generateLegacyContiguousExtraKeys._augment_tokens_for_block_reuse->_resolve_multimodal_run_metadata/_augment_tokens_with_mm_run_metadata/_augment_tokens_with_contiguous_mm_metadata.gen_multimodal_cache_key_tokens(renamed fromgen_multi_modal_tokens) gains atoken_offsetarg so multi-run items don't re-materialize the full per-item synthetic sequence.Backward compatible: when run buffers are absent on a request, the legacy contiguous path runs exactly as before. Run buffers are only consulted when the producer emits them.
Test Coverage
cpp/tests/unit_tests/batch_manager/blockKeyTest.cpp)GenerateExtraKeysUsesExactMultimodalRuns- non-contiguous runs produce per-runMmKeys with correct item-local offsets; gap-only blocks produce no key; second run's offset reflects accumulated prior run lengths.GenerateExtraKeysDisablesHashingForIncompleteRunMetadata- partial run metadata logs a warning and falls back to no extra keys (request continues).cpp/tests/unit_tests/executor/serializeUtilsTest.cpp::MultimodalInputWithUuids) extended to cover the 3 new fields.tests/unittest/_torch/executor/test_kv_cache_v2_multimodal_runs.py) - exact-runs path, out-of-slice run skipping, incomplete-metadataValueError, contiguous-fallback preserved,gen_multimodal_cache_key_tokens(token_offset)semantics.tests/unittest/llmapi/test_llm_kv_cache_events.py::test_multimodal_input_exact_run_buffers) -from_components, paired-fields-required, sum-mismatch, int32 overflow.tests/unittest/bindings/test_executor_bindings.py) -MultimodalInputctor with run buffers, state-tuple round-trip with both 4-tuple (legacy) and 7-tuple (new) state.Performance and accuracy validation
Rule for all perf tables: positive delta means feature is slower.
Main perf numbers
Nemotron Omni synthetic run. 4x GB200.
tp_size=4./v1/completions. No profiler. Each cell has 3 runs x 9 measured requests = 27 measured requests per arm.Takeaway: no clear perf regression. Only positive deltas are
+1.83%and+0.08%, and both ranges cross zero.Extra perf checks
0.9947381.0083181.0087381.0110551.0125201.01100.9599Worst warm repeated ratio observed:
1.012520(+1.25%).Accuracy numbers
40.0, MM keys654, stored blocks74140.0, MM keys654, stored blocks741A, token IDs[32, 151645]A, token IDs[32, 151645]AA, token IDs[32, 151645]A, token IDs[32, 151645]ACCACCAA, token IDs[32, 151645]A, token IDs[32, 151645]DA, token IDs[32, 151645]A, token IDs[32, 151645]DClaim boundary: perf numbers above show no clear regression. Accuracy numbers above show one passing local Video-MME sample plus branch-vs-baseline parity in the other completed video lanes. No scorer-backed Nemotron Omni Video-MME accuracy number yet.
PR Checklist