Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 108 additions & 10 deletions vllm_omni/diffusion/models/gr00t/modeling/processing_gr00t_n1d7.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,108 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor:
return transforms.functional.pad(img, padding=[pad_left, pad_top, pad_right, pad_bottom], fill=0)


class FractionalCenterCrop:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P0] Close #7082 as superseded by already-merged #7083 (d774033, 2026-09-08)

Evidence and suggested fix

Close #7082 as superseded by already-merged #7083 (d774033, 2026-09-08). This head adds FractionalCenterCrop/SmallestMaxSize and a use_albumentations Compose that does transforms.ToImage() then InterpolationMode.BOX resize (processing_gr00t_n1d7.py:128/:136/:140). Gr00tPolicy loads AutoProcessor.from_pretrained (policy.py:76, unchanged) and from_pretrained injects processor_config processor_kwargs; nvidia/GR00T-N1.7-3B ships use_albumentations:true, so every serving video frame hits tensor BOX (torchvision v2: BOX is PIL-only). GitHub reports mergeable=false / mergeable_state=dirty against main 66bbebd because #7083 already landed AlbumentationsEvalImageTransform with cv2.INTER_AREA. Do not keep these helpers in the conflict resolution — that also subsumes the existing L140 INTER_AREA inline.

Evidence: PR-time processing_gr00t_n1d7.py:128 steps = [transforms.ToImage()]; :136 steps.append(SmallestMaxSize(shortest_image_edge, interpolation=transforms.InterpolationMode.BOX)); :140 steps.append(transforms.Resize(size=image_target_size, interpolation=transforms.InterpolationMode.BOX)) — this arm runs when use_albumentations is true. Trigger: unchanged by this diff, present in the PR-time tree: policy.py:76 self.processor: Gr00tN1d7Processor = AutoProcessor.from_pretrained(processor_dir) and processing_gr00t_n1d7.py:619 processor_kwargs = config["processor_kwargs"] / :651 return cls(**processor_kwargs, transformers_loading_kwargs=transformers_loading_kwargs), then :493 image_transform = self.eval_image_transform applied to video frames. nvidia/GR00T-N1.7-3B processor_config.json has "use_albumentations": true, so serving takes the BOX path; torchvision v2 documents BOX as PIL-only after ToImage(), so every Gr00tPolicy frame is sent through an unsupported tensor resize (crash or non-INTER_AREA pixels). GitHub pulls/7082 mergeable=false mergeable_state=dirty vs main 66bbebd; #7083 already merged 2026-09-08 as d774033 with AlbumentationsEvalImageTransform (cv2.INTER_AREA) — not in this PR-time tree (grep has no AlbumentationsEvalImageTransform). Keeping these helpers while resolving the conflict would reintroduce BOX onto main.

"""Center-crop an image by a fractional factor of its current dimensions.

The crop size is computed as int(dim * fraction) for each spatial dim (floor behavior),
matching the behavior used in Isaac-GR00T's FractionalCenterCrop.
"""

def __init__(self, fraction: float):
assert 0.0 < fraction <= 1.0, "fraction must be in (0, 1]"
self.fraction = fraction

def __call__(self, img: torch.Tensor) -> torch.Tensor:
import math

*leading_dims, c, h, w = img.shape
new_h = int(h * self.fraction)
new_w = int(w * self.fraction)
top = (h - new_h) // 2
left = (w - new_w) // 2
if leading_dims:
batch_size = math.prod(leading_dims)
img_r = img.reshape(batch_size, c, h, w)
cropped = img_r[:, :, top : top + new_h, left : left + new_w]
return cropped.reshape(leading_dims + [c, new_h, new_w])
return img[:, top : top + new_h, left : left + new_w]


class SmallestMaxSize:
"""Resize while preserving aspect ratio so that the smallest side equals `size`.

Uses the provided interpolation mode. This mirrors albumentations.SmallestMaxSize.
"""

def __init__(self, size: int, interpolation=transforms.InterpolationMode.BOX):
assert size > 0
self.size = int(size)
self.interpolation = interpolation

def __call__(self, img: torch.Tensor) -> torch.Tensor:
*leading_dims, c, h, w = img.shape
if h == 0 or w == 0:
return img
if min(h, w) == self.size:
return img
scale = self.size / float(min(h, w))
new_h = int(round(h * scale))
new_w = int(round(w * scale))
# Use torchvision Resize with explicit (H, W)
resize = transforms.Resize(size=(new_h, new_w), interpolation=self.interpolation)
if leading_dims:
batch_size = int(torch.tensor(leading_dims).prod().item()) if leading_dims else 1
img_r = img.reshape(batch_size, c, h, w)
resized = resize(img_r)
return resized.reshape(leading_dims + [c, new_h, new_w])
return resize(img)


def _build_eval_image_transform(
image_target_size: list[int],
image_crop_size: list[int],
use_albumentations: bool = False,
letter_box_transform: bool = True,
crop_fraction: float = 0.95,
shortest_image_edge: int = 256,
) -> transforms.Compose:
"""Deterministic eval/inference image transform (letterbox → resize → centercrop → resize)."""
return transforms.Compose(
[
transforms.ToImage(),
LetterBoxTransform(),
transforms.Resize(size=image_target_size),
transforms.CenterCrop(size=image_crop_size),
transforms.Resize(size=image_target_size),
]
)
"""Deterministic eval/inference image transform.

Two modes are supported to match upstream Isaac-GR00T checkpoints:
- use_albumentations=True: emulates the albumentations recipe used in Isaac-GR00T
(optional letterbox -> SmallestMaxSize(shortest_image_edge, INTER_AREA) ->
FractionalCenterCrop(crop_fraction) -> Resize(image_target_size, INTER_AREA)).
- use_albumentations=False: preserves the original vLLM-Omni pipeline
(optional letterbox -> Resize(image_target_size) -> CenterCrop(image_crop_size)
-> Resize(image_target_size)).

The function intentionally avoids importing albumentations to keep the repo
dependency-free and reproduces the deterministic behaviour using torchvision
transforms and small helpers above.
"""
steps = [transforms.ToImage()]

if use_albumentations:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] The new use_albumentations=True arm (processing_gr00t_n1d7.py:130) appends…

Evidence and suggested fix

The new use_albumentations=True arm (processing_gr00t_n1d7.py:130) appends SmallestMaxSize(..., InterpolationMode.BOX) then Resize(..., BOX) after ToImage(). Gr00tPolicy loads via AutoProcessor.from_pretrained (policy.py:76, unchanged) and from_pretrained does cls(**processor_kwargs) (:651), so checkpoint flags use_albumentations: true / letter_box_transform: false select this arm. SmallestMaxSize early-returns only when min(h, w) == self.size (:91, default 256); DROID handshake Gr00tN1d7.yaml:22 image_resolution: [180, 320] does not, so the BOX resize runs. This PR adds no test of _build_eval_image_transform; test_pipeline.py monkeypatches Gr00tPolicy, and tests/helpers/client.py:162 uses 256×256 frames that skip that resize (int(256*0.95)=243 crop is frame[6:249, 6:249]). Add a CPU unit test of _build_eval_image_transform(use_albumentations=True, letter_box_transform=False) vs cv2.INTER_AREA on that 256×256 crop and on a 180×320 frame so SmallestMaxSize cannot regress; it should fail at this head until both BOX resizes are replaced.

Evidence: Trigger: _build_eval_image_transform(use_albumentations=True, letter_box_transform=False) (selected when checkpoint processor_kwargs set those flags) on a frame whose min side is not 256, including the DROID handshake size. Adverse/unmet: SmallestMaxSize does not early-return and applies tensor Resize(..., BOX) after ToImage(); this PR adds that arm with zero tests, and the in-tree 256×256 helper skips the resize. processing_gr00t_n1d7.py:128 steps = [transforms.ToImage()]; :130 if use_albumentations:; :136 steps.append(SmallestMaxSize(shortest_image_edge, interpolation=transforms.InterpolationMode.BOX)); :91 if min(h, w) == self.size: return img; :97 resize = transforms.Resize(size=(new_h, new_w), interpolation=self.interpolation). Unchanged by this diff, present in the PR-time tree: policy.py:76 self.processor: Gr00tN1d7Processor = AutoProcessor.from_pretrained(processor_dir); processing_gr00t_n1d7.py:651 return cls(**processor_kwargs, transformers_loading_kwargs=transformers_loading_kwargs); Gr00tN1d7.yaml:22 image_resolution: [180, 320]; tests/diffusion/models/gr00t/test_pipeline.py:58 monkeypatch.setattr(pipeline_gr00t, "Gr00tPolicy", FakeGr00tPolicy); tests/helpers/client.py:162 "exterior_image_1_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8).

# Only apply letterbox when explicitly requested in the processor config.
if letter_box_transform:
steps.append(LetterBoxTransform())
if shortest_image_edge is not None:
# Use BOX (area-like) interpolation to match cv2.INTER_AREA used by Isaac.
steps.append(SmallestMaxSize(shortest_image_edge, interpolation=transforms.InterpolationMode.BOX))
if crop_fraction is not None:
steps.append(FractionalCenterCrop(crop_fraction))
# Resize back to target size using BOX/area-like interpolation.
steps.append(transforms.Resize(size=image_target_size, interpolation=transforms.InterpolationMode.BOX))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use cv2.INTER_AREA for both resizes; torchvision tensor resize rejects BOX.

else:
# Preserve historical vLLM-Omni behavior for callers that don't opt into
# the albumentations-style pipeline.
if letter_box_transform:
steps.append(LetterBoxTransform())
steps.append(transforms.Resize(size=image_target_size))
steps.append(transforms.CenterCrop(size=image_crop_size))
steps.append(transforms.Resize(size=image_target_size))

return transforms.Compose(steps)


logger = init_logger(__name__)
Expand Down Expand Up @@ -176,6 +264,8 @@ def __init__(
image_target_size: list[int] = None,
shortest_image_edge: int = 256,
crop_fraction: float = 0.95,
use_albumentations: bool = False,
letter_box_transform: bool = True,
random_rotation_angle: int | None = None,
color_jitter_params: dict[str, float] | None = None,
formalize_language: bool = True,
Expand Down Expand Up @@ -237,13 +327,19 @@ def __init__(
self.embodiment_id_mapping[k] = v
self.shortest_image_edge = shortest_image_edge
self.crop_fraction = crop_fraction
self.use_albumentations = use_albumentations
self.letter_box_transform = letter_box_transform

self.statistics: dict[str, dict[str, dict[str, dict[str, list[float]]]]] = {}

# Eval/inference image transform
self.eval_image_transform = _build_eval_image_transform(
image_target_size,
image_crop_size,
use_albumentations=self.use_albumentations,
letter_box_transform=self.letter_box_transform,
crop_fraction=self.crop_fraction,
shortest_image_edge=self.shortest_image_edge,
)
self._collator = self.data_collator_class(
model_name=model_name,
Expand Down Expand Up @@ -470,6 +566,8 @@ def save_pretrained(self, save_directory: str | Path) -> list[Path]:
"color_jitter_params": self.color_jitter_params,
"shortest_image_edge": self.shortest_image_edge,
"crop_fraction": self.crop_fraction,
"use_albumentations": self.use_albumentations,
"letter_box_transform": self.letter_box_transform,
"model_name": self.model_name,
"model_type": self.model_type,
"formalize_language": self.formalize_language,
Expand Down
Loading