Reduce recognizer VRAM by discarding logits per chunk
Browse filesComputes argmax + softmax probability of the winning token inside
the recognizer chunk loop and discards the full [N, T, num_tokens]
logits tensor immediately, reducing peak VRAM.
Also converts region vertices to numpy during graph_build to avoid
per-region .cpu() calls in post-processing.
nemotron-ocr/src/nemotron_ocr/inference/pipeline_v2.py
CHANGED
|
@@ -409,23 +409,28 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 409 |
"""Run recognizer in fixed-size region chunks.
|
| 410 |
|
| 411 |
Every chunk is padded to exactly ``recognizer_chunk_size`` regions
|
| 412 |
-
so CUDA always sees the same tensor shape
|
| 413 |
-
|
|
|
|
|
|
|
|
|
|
| 414 |
|
| 415 |
Returns:
|
| 416 |
-
|
| 417 |
-
|
|
|
|
| 418 |
"""
|
| 419 |
n = rec_quads.shape[0]
|
| 420 |
if n == 0:
|
| 421 |
device = rec_quads.device
|
| 422 |
return (
|
| 423 |
-
torch.empty(0, self.max_width,
|
|
|
|
| 424 |
torch.empty(0, self.max_width, self.recognizer.feature_depth, dtype=torch.float16, device=device),
|
| 425 |
)
|
| 426 |
|
| 427 |
cs = self.recognizer_chunk_size
|
| 428 |
-
|
| 429 |
with amp.autocast("cuda", enabled=True), torch.inference_mode():
|
| 430 |
for start in range(0, n, cs):
|
| 431 |
end = min(start + cs, n)
|
|
@@ -434,13 +439,21 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 434 |
if real_n < cs:
|
| 435 |
pad = torch.zeros(cs - real_n, *chunk.shape[1:], dtype=chunk.dtype, device=chunk.device)
|
| 436 |
chunk = torch.cat([chunk, pad], dim=0)
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
-
|
| 442 |
-
return
|
| 443 |
-
|
|
|
|
| 444 |
|
| 445 |
# ------------------------------------------------------------------
|
| 446 |
# Phase 6 — Relational model + build output dict
|
|
@@ -453,12 +466,17 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 453 |
region_counts,
|
| 454 |
region_counts_cpu,
|
| 455 |
rec_features,
|
| 456 |
-
|
|
|
|
| 457 |
e2e_det_conf,
|
| 458 |
confidence,
|
| 459 |
padded_lengths,
|
| 460 |
):
|
| 461 |
-
"""Run relational model and assemble the output dict
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
with amp.autocast("cuda", enabled=True), torch.inference_mode():
|
| 463 |
rel_output = self.relational(
|
| 464 |
rel_quads,
|
|
@@ -481,12 +499,9 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 481 |
scale_per_region = torch.repeat_interleave(scale_factors, counts_long, dim=0)
|
| 482 |
quads_scaled = quads_cuda * scale_per_region.view(-1, 1, 1)
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
seq_ids = rec_logits.argmax(dim=2)
|
| 487 |
-
seq_probs = (rec_logits.gather(2, seq_ids.unsqueeze(2)) - log_norm).squeeze(2).exp()
|
| 488 |
|
| 489 |
-
# Pre-compute text confidence on GPU (geometric mean of per-token probs)
|
| 490 |
_before_eos = (seq_ids == 1).cumsum(dim=1) == 0
|
| 491 |
_real = (seq_ids != 0) & _before_eos
|
| 492 |
_counts = _real.sum(dim=1).clamp(min=1).float()
|
|
@@ -510,48 +525,19 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 510 |
"e2e_recog_features": rec_features,
|
| 511 |
}
|
| 512 |
|
| 513 |
-
return output
|
| 514 |
|
| 515 |
# ------------------------------------------------------------------
|
| 516 |
# Phase 7 — Post-process
|
| 517 |
# ------------------------------------------------------------------
|
| 518 |
|
| 519 |
-
def _decode_with_fallback(self, output
|
| 520 |
-
"""
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
probabilities (dim=3). We try the fast path first and cache the
|
| 525 |
-
result so subsequent calls skip the try/except.
|
| 526 |
-
"""
|
| 527 |
-
if getattr(self, "_use_full_softmax", False):
|
| 528 |
-
output = dict(output)
|
| 529 |
-
output["sequences"] = F.softmax(rec_output.float(), dim=2)
|
| 530 |
-
output.pop("sequence_probs", None)
|
| 531 |
-
return self.recog_encoder.convert_targets_to_labels(
|
| 532 |
-
output, image_size=None, is_gt=False,
|
| 533 |
-
)
|
| 534 |
-
|
| 535 |
-
try:
|
| 536 |
-
batch = self.recog_encoder.convert_targets_to_labels(
|
| 537 |
-
output, image_size=None, is_gt=False,
|
| 538 |
-
)
|
| 539 |
-
self._use_full_softmax = False
|
| 540 |
-
return batch
|
| 541 |
-
except (ValueError, RuntimeError):
|
| 542 |
-
logger.info(
|
| 543 |
-
"Encoder does not support argmax sequences; "
|
| 544 |
-
"falling back to full softmax (one-time detection)."
|
| 545 |
-
)
|
| 546 |
-
self._use_full_softmax = True
|
| 547 |
-
output = dict(output)
|
| 548 |
-
output["sequences"] = F.softmax(rec_output.float(), dim=2)
|
| 549 |
-
output.pop("sequence_probs", None)
|
| 550 |
-
return self.recog_encoder.convert_targets_to_labels(
|
| 551 |
-
output, image_size=None, is_gt=False,
|
| 552 |
-
)
|
| 553 |
|
| 554 |
-
def _postprocess_batch(self, output,
|
| 555 |
"""Decode sequences, build relation graphs, format per-image results."""
|
| 556 |
import gc
|
| 557 |
gc_was_enabled = gc.isenabled()
|
|
@@ -559,17 +545,17 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 559 |
|
| 560 |
try:
|
| 561 |
return self._postprocess_batch_inner(
|
| 562 |
-
output,
|
| 563 |
)
|
| 564 |
finally:
|
| 565 |
if gc_was_enabled:
|
| 566 |
gc.enable()
|
| 567 |
|
| 568 |
-
def _postprocess_batch_inner(self, output,
|
| 569 |
_t = time.perf_counter
|
| 570 |
|
| 571 |
t0 = _t()
|
| 572 |
-
batch = self._decode_with_fallback(output
|
| 573 |
recog_decode_ms = (_t() - t0) * 1000
|
| 574 |
|
| 575 |
t0 = _t()
|
|
@@ -595,7 +581,8 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 595 |
|
| 596 |
for example in batch:
|
| 597 |
for text_region in example:
|
| 598 |
-
|
|
|
|
| 599 |
graph_build_ms = (_t() - t0) * 1000
|
| 600 |
|
| 601 |
t0 = _t()
|
|
@@ -679,7 +666,12 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 679 |
|
| 680 |
# Phase 2.5: centerness + peak prefilter
|
| 681 |
if self._use_prefilter:
|
|
|
|
| 682 |
det_conf = self._prefilter_detections(det_conf, det_rboxes)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
# Phase 3: NMS
|
| 685 |
t0 = time.perf_counter()
|
|
@@ -709,22 +701,23 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 709 |
torch.cuda.synchronize()
|
| 710 |
T["rectify"] = (time.perf_counter() - t0) * 1000
|
| 711 |
|
| 712 |
-
# Phase 5: recognizer
|
| 713 |
t0 = time.perf_counter()
|
| 714 |
-
|
| 715 |
del rec_quads
|
| 716 |
torch.cuda.synchronize()
|
| 717 |
T["recognizer"] = (time.perf_counter() - t0) * 1000
|
| 718 |
|
| 719 |
# Phase 6: relational model
|
| 720 |
t0 = time.perf_counter()
|
| 721 |
-
output
|
| 722 |
rel_quads,
|
| 723 |
quads_cuda,
|
| 724 |
region_counts,
|
| 725 |
region_counts_cpu,
|
| 726 |
rec_features,
|
| 727 |
-
|
|
|
|
| 728 |
e2e_det_conf,
|
| 729 |
confidence,
|
| 730 |
padded_lengths,
|
|
@@ -736,7 +729,7 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 736 |
# Phase 7: postprocess (broken down into sub-phases)
|
| 737 |
t0 = time.perf_counter()
|
| 738 |
results = self._postprocess_batch(
|
| 739 |
-
output,
|
| 740 |
)
|
| 741 |
T["post_total"] = (time.perf_counter() - t0) * 1000
|
| 742 |
|
|
@@ -747,13 +740,13 @@ class NemotronOCRV2(NemotronOCR):
|
|
| 747 |
|
| 748 |
logger.info(
|
| 749 |
"batch=%d regions=%d(%s) "
|
| 750 |
-
"img_load=%.1f gpu_pre=%.1f det=%.1f nms=%.1f "
|
| 751 |
"rectify=%.1f recog=%.1f rel=%.1f "
|
| 752 |
"post[decode=%.1f rel=%.1f graph=%.1f fmt=%.1f]=%.1f "
|
| 753 |
"total=%.1f ms",
|
| 754 |
num_images, total_regions, counts_str,
|
| 755 |
T.get("img_load", 0), T.get("gpu_preproc", 0),
|
| 756 |
-
T["detector"], T["nms"],
|
| 757 |
T["rectify"], T["recognizer"], T["relational"],
|
| 758 |
T.get("post_recog_decode", 0), T.get("post_rel_decode", 0),
|
| 759 |
T.get("post_graph_build", 0), T.get("post_format", 0),
|
|
|
|
| 409 |
"""Run recognizer in fixed-size region chunks.
|
| 410 |
|
| 411 |
Every chunk is padded to exactly ``recognizer_chunk_size`` regions
|
| 412 |
+
so CUDA always sees the same tensor shape.
|
| 413 |
+
|
| 414 |
+
Computes argmax + softmax probability of the winning token inside
|
| 415 |
+
each chunk loop and discards the full logits immediately, reducing
|
| 416 |
+
peak VRAM from ``[N, T, num_tokens]`` to ``[chunk_size, T, num_tokens]``.
|
| 417 |
|
| 418 |
Returns:
|
| 419 |
+
rec_ids: (N_regions, T) int64 — argmax token indices
|
| 420 |
+
rec_probs: (N_regions, T) float32 — softmax probability of winning token
|
| 421 |
+
rec_features: (N_regions, T, D) — transformer features for relational model
|
| 422 |
"""
|
| 423 |
n = rec_quads.shape[0]
|
| 424 |
if n == 0:
|
| 425 |
device = rec_quads.device
|
| 426 |
return (
|
| 427 |
+
torch.empty(0, self.max_width, dtype=torch.int64, device=device),
|
| 428 |
+
torch.empty(0, self.max_width, dtype=torch.float32, device=device),
|
| 429 |
torch.empty(0, self.max_width, self.recognizer.feature_depth, dtype=torch.float16, device=device),
|
| 430 |
)
|
| 431 |
|
| 432 |
cs = self.recognizer_chunk_size
|
| 433 |
+
ids_parts, probs_parts, feat_parts = [], [], []
|
| 434 |
with amp.autocast("cuda", enabled=True), torch.inference_mode():
|
| 435 |
for start in range(0, n, cs):
|
| 436 |
end = min(start + cs, n)
|
|
|
|
| 439 |
if real_n < cs:
|
| 440 |
pad = torch.zeros(cs - real_n, *chunk.shape[1:], dtype=chunk.dtype, device=chunk.device)
|
| 441 |
chunk = torch.cat([chunk, pad], dim=0)
|
| 442 |
+
logits, feats = self.recognizer(chunk)
|
| 443 |
+
|
| 444 |
+
ids = logits.argmax(dim=2)
|
| 445 |
+
probs = torch.softmax(logits, dim=2).gather(
|
| 446 |
+
2, ids.unsqueeze(2)
|
| 447 |
+
).squeeze(2).float()
|
| 448 |
+
|
| 449 |
+
ids_parts.append(ids[:real_n])
|
| 450 |
+
probs_parts.append(probs[:real_n])
|
| 451 |
+
feat_parts.append(feats[:real_n])
|
| 452 |
|
| 453 |
+
def _cat_or_single(parts):
|
| 454 |
+
return parts[0] if len(parts) == 1 else torch.cat(parts, dim=0)
|
| 455 |
+
|
| 456 |
+
return _cat_or_single(ids_parts), _cat_or_single(probs_parts), _cat_or_single(feat_parts)
|
| 457 |
|
| 458 |
# ------------------------------------------------------------------
|
| 459 |
# Phase 6 — Relational model + build output dict
|
|
|
|
| 466 |
region_counts,
|
| 467 |
region_counts_cpu,
|
| 468 |
rec_features,
|
| 469 |
+
rec_ids,
|
| 470 |
+
rec_probs,
|
| 471 |
e2e_det_conf,
|
| 472 |
confidence,
|
| 473 |
padded_lengths,
|
| 474 |
):
|
| 475 |
+
"""Run relational model and assemble the output dict.
|
| 476 |
+
|
| 477 |
+
Accepts pre-computed argmax ids and per-token probabilities from the
|
| 478 |
+
recognizer (avoids materializing the full logits tensor).
|
| 479 |
+
"""
|
| 480 |
with amp.autocast("cuda", enabled=True), torch.inference_mode():
|
| 481 |
rel_output = self.relational(
|
| 482 |
rel_quads,
|
|
|
|
| 499 |
scale_per_region = torch.repeat_interleave(scale_factors, counts_long, dim=0)
|
| 500 |
quads_scaled = quads_cuda * scale_per_region.view(-1, 1, 1)
|
| 501 |
|
| 502 |
+
seq_ids = rec_ids
|
| 503 |
+
seq_probs = rec_probs
|
|
|
|
|
|
|
| 504 |
|
|
|
|
| 505 |
_before_eos = (seq_ids == 1).cumsum(dim=1) == 0
|
| 506 |
_real = (seq_ids != 0) & _before_eos
|
| 507 |
_counts = _real.sum(dim=1).clamp(min=1).float()
|
|
|
|
| 525 |
"e2e_recog_features": rec_features,
|
| 526 |
}
|
| 527 |
|
| 528 |
+
return output
|
| 529 |
|
| 530 |
# ------------------------------------------------------------------
|
| 531 |
# Phase 7 — Post-process
|
| 532 |
# ------------------------------------------------------------------
|
| 533 |
|
| 534 |
+
def _decode_with_fallback(self, output):
|
| 535 |
+
"""Decode recognized sequences using pre-computed argmax indices."""
|
| 536 |
+
return self.recog_encoder.convert_targets_to_labels(
|
| 537 |
+
output, image_size=None, is_gt=False,
|
| 538 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
|
| 540 |
+
def _postprocess_batch(self, output, original_shapes, merge_level, include_invalid, timings=None):
|
| 541 |
"""Decode sequences, build relation graphs, format per-image results."""
|
| 542 |
import gc
|
| 543 |
gc_was_enabled = gc.isenabled()
|
|
|
|
| 545 |
|
| 546 |
try:
|
| 547 |
return self._postprocess_batch_inner(
|
| 548 |
+
output, original_shapes, merge_level, include_invalid, timings,
|
| 549 |
)
|
| 550 |
finally:
|
| 551 |
if gc_was_enabled:
|
| 552 |
gc.enable()
|
| 553 |
|
| 554 |
+
def _postprocess_batch_inner(self, output, original_shapes, merge_level, include_invalid, timings=None):
|
| 555 |
_t = time.perf_counter
|
| 556 |
|
| 557 |
t0 = _t()
|
| 558 |
+
batch = self._decode_with_fallback(output)
|
| 559 |
recog_decode_ms = (_t() - t0) * 1000
|
| 560 |
|
| 561 |
t0 = _t()
|
|
|
|
| 581 |
|
| 582 |
for example in batch:
|
| 583 |
for text_region in example:
|
| 584 |
+
v = text_region.region.vertices
|
| 585 |
+
text_region.region = v.cpu().numpy() if hasattr(v, 'cpu') else np.asarray(v)
|
| 586 |
graph_build_ms = (_t() - t0) * 1000
|
| 587 |
|
| 588 |
t0 = _t()
|
|
|
|
| 666 |
|
| 667 |
# Phase 2.5: centerness + peak prefilter
|
| 668 |
if self._use_prefilter:
|
| 669 |
+
t0 = time.perf_counter()
|
| 670 |
det_conf = self._prefilter_detections(det_conf, det_rboxes)
|
| 671 |
+
torch.cuda.synchronize()
|
| 672 |
+
T["prefilter"] = (time.perf_counter() - t0) * 1000
|
| 673 |
+
else:
|
| 674 |
+
T["prefilter"] = 0.0
|
| 675 |
|
| 676 |
# Phase 3: NMS
|
| 677 |
t0 = time.perf_counter()
|
|
|
|
| 701 |
torch.cuda.synchronize()
|
| 702 |
T["rectify"] = (time.perf_counter() - t0) * 1000
|
| 703 |
|
| 704 |
+
# Phase 5: recognizer (argmax + softmax prob per chunk, discards full logits)
|
| 705 |
t0 = time.perf_counter()
|
| 706 |
+
rec_ids, rec_probs, rec_features = self._run_recognizer_chunked(rec_quads)
|
| 707 |
del rec_quads
|
| 708 |
torch.cuda.synchronize()
|
| 709 |
T["recognizer"] = (time.perf_counter() - t0) * 1000
|
| 710 |
|
| 711 |
# Phase 6: relational model
|
| 712 |
t0 = time.perf_counter()
|
| 713 |
+
output = self._run_relational_and_build_output(
|
| 714 |
rel_quads,
|
| 715 |
quads_cuda,
|
| 716 |
region_counts,
|
| 717 |
region_counts_cpu,
|
| 718 |
rec_features,
|
| 719 |
+
rec_ids,
|
| 720 |
+
rec_probs,
|
| 721 |
e2e_det_conf,
|
| 722 |
confidence,
|
| 723 |
padded_lengths,
|
|
|
|
| 729 |
# Phase 7: postprocess (broken down into sub-phases)
|
| 730 |
t0 = time.perf_counter()
|
| 731 |
results = self._postprocess_batch(
|
| 732 |
+
output, original_shapes, merge_level, include_invalid, timings=T,
|
| 733 |
)
|
| 734 |
T["post_total"] = (time.perf_counter() - t0) * 1000
|
| 735 |
|
|
|
|
| 740 |
|
| 741 |
logger.info(
|
| 742 |
"batch=%d regions=%d(%s) "
|
| 743 |
+
"img_load=%.1f gpu_pre=%.1f det=%.1f prefilt=%.1f nms=%.1f "
|
| 744 |
"rectify=%.1f recog=%.1f rel=%.1f "
|
| 745 |
"post[decode=%.1f rel=%.1f graph=%.1f fmt=%.1f]=%.1f "
|
| 746 |
"total=%.1f ms",
|
| 747 |
num_images, total_regions, counts_str,
|
| 748 |
T.get("img_load", 0), T.get("gpu_preproc", 0),
|
| 749 |
+
T["detector"], T["prefilter"], T["nms"],
|
| 750 |
T["rectify"], T["recognizer"], T["relational"],
|
| 751 |
T.get("post_recog_decode", 0), T.get("post_rel_decode", 0),
|
| 752 |
T.get("post_graph_build", 0), T.get("post_format", 0),
|