Skip to content

Add DexSuite ResNet camera presets - #5453

Closed
ClawLabby wants to merge 12 commits into
isaac-sim:developfrom
ClawLabby:pr/v4/dexsuite-resnet-features
Closed

Add DexSuite ResNet camera presets#5453
ClawLabby wants to merge 12 commits into
isaac-sim:developfrom
ClawLabby:pr/v4/dexsuite-resnet-features

Conversation

@ClawLabby

Copy link
Copy Markdown

Summary

  • add shared ResNet observation preparation utilities
  • add DexSuite single-camera ResNet environment and RSL-RL agent presets
  • wire the base DexSuite Lift task so presets=cube,resnet_single_camera,isaacsim_rtx_renderer selects the matching env and agent config through the default entry point

Validation

  • PYTHONDONTWRITEBYTECODE=1 env_isaaclab/bin/python -m pytest -q source/isaaclab_tasks/test/test_dexsuite_agent_presets.py

Notes

  • Opening as draft while cross-renderer / scaling benchmark validation continues.

@github-actions github-actions Bot added enhancement New feature or request isaac-lab Related to Isaac Lab team labels Apr 30, 2026

@isaaclab-review-bot isaaclab-review-bot Bot 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.

🤖 Isaac Lab Review Bot

Summary

This PR adds ResNet feature extraction utilities for DexSuite environments, factoring out the ResNet model preparation into a standalone module (resnet_utils.py) that can be tested without simulation context. The implementation correctly removes the FC classification layer to output feature vectors (512-dim for ResNet18/34, 2048-dim for ResNet50/101) instead of the previous 1000-dim classification logits, adds proper torch.no_grad() wrapping for frozen inference, and introduces a freeze parameter for optional gradient flow. The DexSuite task configurations are extended with ResNet single-camera observation and agent presets.

Architecture Impact

  • resnet_utils.py: New standalone module with zero Isaac Lab dependencies — clean separation enables lightweight unit testing
  • observations.py: image_features class now delegates to resnet_utils.prepare_resnet_model(), maintaining backward compatibility while fixing the feature dimension issue
  • DexSuite configs: New resnet_single_camera preset propagates through scene, observations, and agent configs via the existing PresetCfg machinery
  • Breaking change: Policies trained with the previous 1000-dim ResNet output are incompatible (documented in CHANGELOG)

Implementation Verdict

Minor fixes needed — The core implementation is correct and well-tested, but there are a few issues with the freeze parameter closure capture and missing module exports.

Test Coverage

Good for the new utility, incomplete for integration. The test_resnet_model_preparation.py tests thoroughly cover the prepare_resnet_model function contract (FC removal, feature dimensions, freeze behavior, error handling). However, the test_dexsuite_agent_presets.py only validates config resolution, not actual inference. Missing: end-to-end test that verifies ResNet features flow correctly through the observation manager in a live environment.

CI Status

No CI checks available yet — cannot verify tests pass.

Findings

🔴 Critical: source/isaaclab/isaaclab/envs/mdp/resnet_utils.py:65-67 — Closure captures freeze from outer scope, but model is loaded once while inference is called many times

The _inference function captures freeze from the enclosing scope at definition time. If someone calls prepare_resnet_model(..., freeze=True), stores the result, then later somehow expects to toggle freeze behavior, it won't work. More importantly, there's a subtle issue: the model parameter is passed to _inference, but the model_device is captured from closure. If the model is moved to a different device after creation, _inference will still normalize tensors on the original model_device.

def _inference(model, images: torch.Tensor) -> torch.Tensor:
    image_proc = images.to(model_device)  # model_device captured from closure

This is acceptable for current usage patterns but could cause silent device mismatches. Consider using next(model.parameters()).device instead.

🟡 Warning: source/isaaclab/isaaclab/envs/mdp/init.py — Missing export of resnet_utils module

The new resnet_utils.py module is not exported from the isaaclab.envs.mdp package. While it's imported internally by observations.py, external code importing from isaaclab.envs.mdp cannot access resnet_utils directly. The test file imports via from isaaclab.envs.mdp.resnet_utils import prepare_resnet_model which works due to Python's module resolution, but this bypasses the package's public API.

Add to __init__.py or __init__.pyi:

from isaaclab.envs.mdp import resnet_utils

🟡 Warning: source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/camera_cfg.py:217-219 — __post_init__ iterates all dataclass fields including non-observation groups

def __post_init__(self):
    super().__post_init__()
    for group in self.__dataclass_fields__.values():
        obs_group = getattr(self, group.name)
        obs_group.history_length = None

This iterates over ALL fields in the dataclass, but not all fields may be ObsGroup instances. If a future field is added that isn't an ObsGroup, this will raise AttributeError. The pattern in SingleCameraObservationsCfg at line 177-180 has the same issue. Consider adding a type check:

for group in self.__dataclass_fields__.values():
    obs_group = getattr(self, group.name)
    if isinstance(obs_group, ObsGroup):
        obs_group.history_length = None

🟡 Warning: source/isaaclab_tasks/test/test_dexsuite_agent_presets.py:17-22 — Test manipulates sys.argv without thread safety

The test modifies sys.argv globally which could cause issues in parallel test execution:

def _resolve_lift_with_presets(presets: str):
    old_argv = sys.argv.copy()
    try:
        sys.argv = [sys.argv[0], f"presets={presets}"]
        return resolve_task_config(...)
    finally:
        sys.argv = old_argv

While the try/finally ensures restoration, pytest may run tests in parallel. Consider using unittest.mock.patch.object(sys, 'argv', [...]) for proper isolation.

🔵 Improvement: source/isaaclab/isaaclab/envs/mdp/resnet_utils.py:75-76 — Redundant tensor creation on every inference call

mean = torch.tensor([0.485, 0.456, 0.406], device=model_device, dtype=torch.float32).view(1, 3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225], device=model_device, dtype=torch.float32).view(1, 3, 1, 1)

These normalization constants are created fresh on every inference call. For high-frequency observation queries, this adds unnecessary allocation overhead. Cache them in the closure:

_mean = torch.tensor([0.485, 0.456, 0.406], device=model_device, dtype=torch.float32).view(1, 3, 1, 1)
_std = torch.tensor([0.229, 0.224, 0.225], device=model_device, dtype=torch.float32).view(1, 3, 1, 1)

def _inference(model, images: torch.Tensor) -> torch.Tensor:
    # use _mean, _std from closure

🔵 Improvement: source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_resnet_cfg.py:38-39 — Hardcoded observation group names should reference a constant

obs_groups = {"actor": ["policy", "proprio", "resnet_features"], "critic": ["policy", "proprio", "resnet_features"]}

The string "resnet_features" must exactly match the attribute name in ResNetSingleCameraObservationsCfg.resnet_features. If the observation group is renamed, this will silently break. Consider defining a shared constant or adding a validation check.

🔵 Improvement: source/isaaclab/docs/CHANGELOG.rst:4-18 — CHANGELOG states breaking change but no deprecation warning emitted

The CHANGELOG correctly documents the breaking change from 1000-dim to 512-dim output, but the code doesn't emit any runtime warning for users who might have trained policies with the old behavior. Consider adding a one-time deprecation notice in the first release after merge, or at minimum a more prominent warning in the image_features docstring.

@isaaclab-review-bot isaaclab-review-bot Bot 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.

🤖 Isaac Lab Review Bot

Summary

This follow-up review confirms that the previous concerns have been addressed in the new commits.

Previous Concerns Addressed

  1. Device mismatch in _inference — Now dynamically retrieves device via _get_model_device(model) instead of capturing model_device from closure
  2. Missing resnet_utils export — Added to __init__.pyi with both from . import resnet_utils and inclusion in __all__
  3. __post_init__ type safety — Both SingleCameraObservationsCfg and ResNetSingleCameraObservationsCfg now include if isinstance(obs_group, ObsGroup) checks
  4. sys.argv thread safety in tests — Now uses mock.patch.object(sys, 'argv', ...) for proper isolation
  5. Redundant tensor creation — Normalization tensors are now cached via _get_normalization_tensors() with device-keyed dictionary

Implementation Verdict

Ship it — All critical and warning-level concerns from the previous review have been properly addressed. The code is clean, well-tested, and follows good practices.

…on policies

Adds frozen ResNet18 feature extraction at the observation level for
DexSuite Kuka Allegro manipulation tasks. The ResNet is framework-agnostic
and runs outside the policy network.

New Features:
- Add DexsuiteKukaAllegroLiftSingleCameraResNetEnvCfg: env with frozen ResNet18
  feature extraction (512-dim, ImageNet pretrained) as policy observations
- Add DexsuiteKukaAllegroPPOResNetRunnerCfg in rsl_rl_resnet_cfg.py for rsl_rl
- Register Isaac-Dexsuite-Kuka-Allegro-Lift-Single-Camera-ResNet-v0 gym env

Bug Fixes:
- Fix _prepare_resnet_model: remove FC layer to extract features (512-dim for
  ResNet18/34, 2048-dim for ResNet50/101) instead of classification logits (1000-dim)
- Fix _prepare_resnet_model: use proper weights enum (models.ResNet18_Weights.IMAGENET1K_V1)
  instead of deprecated string form
- Add torch.no_grad() context in ResNet inference path

Breaking Changes:
- image_features with ResNet models now outputs feature vectors instead of
  classification logits. Policies trained with the previous behavior must be retrained.

Tests:
- Add test_resnet_model_preparation.py: verifies FC removal, feature dims (512/2048),
  no_grad inference, eval mode (no Kit required)
…based obs selection

- Extract pure torchvision logic to resnet_utils.py (no sim dependency)
- Add freeze=True param to image_features and prepare_resnet_model
- Rewrite test to import from resnet_utils directly (no duplication)
- Add ResNetSingleCameraObservationsCfg to camera_cfg.py
- Add resnet_single_camera preset to KukaAllegroObservationCfg
- Remove dexsuite_kuka_allegro_vision_env_cfg.py (superseded by preset)
- Remove ResNet-specific task registrations (use base task + presets)
- Fix CHANGELOG: version 4.6.17, fold breaking change into Changed
- Fix rsl_rl_resnet_cfg.py docstring (runner is RSL-RL specific)
@ClawLabby
ClawLabby force-pushed the pr/v4/dexsuite-resnet-features branch from c6d2cd3 to 2531ee1 Compare April 30, 2026 22:28
@ClawLabby
ClawLabby marked this pull request as ready for review April 30, 2026 22:28

@isaaclab-review-bot isaaclab-review-bot Bot 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.

🤖 Isaac Lab Review Bot

Summary

All previous concerns have been addressed. The code is ready to ship.

Implementation Verdict

Ship it — The follow-up commits properly addressed all five concerns from the previous review: device mismatch fix via _get_model_device(), resnet_utils export in __init__.pyi, type safety checks in __post_init__, proper sys.argv mocking in tests, and normalization tensor caching. No new issues introduced.

@greptile-apps

greptile-apps Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces resnet_utils.prepare_resnet_model (a simulation-free torchvision helper that replaces the FC layer with Identity and optionally freezes the backbone), wires it into image_features, and adds matching DexSuite Kuka Allegro scene/observation/agent presets for a single-camera ResNet18 workflow.

  • P1 — gradient flow advertised but silently broken: image_features.__call__ calls features.detach() unconditionally (line 551 of observations.py), so freeze=False never actually allows gradient flow through the backbone despite being documented in the CHANGELOG as a new capability. The unit tests only exercise resnet_utils._inference directly and do not catch this.
  • P2 — no resize to 224×224: The _inference function applies ImageNet normalization but never resizes the 64×64 camera output to the 224×224 resolution the pretrained weights expect, leaving feature quality uncharacterised at training resolution.

Confidence Score: 3/5

Not safe to merge as-is: the freeze=False gradient-flow feature is silently broken by an unconditional .detach() in the observation manager.

One P1 defect (detach cuts gradient flow for freeze=False) contradicts documented CHANGELOG behaviour. Core frozen-ResNet RL workflow still works correctly; only the unfrozen gradient path is broken. Plus one P2 (missing resize).

source/isaaclab/isaaclab/envs/mdp/observations.py — the unconditional features.detach() on line 551 needs to be made conditional on self.freeze.

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/envs/mdp/observations.py Delegates ResNet preparation to resnet_utils and threads freeze through, but features.detach() on line 551 is unconditional, silently breaking the advertised freeze=False gradient-flow use case.
source/isaaclab/isaaclab/envs/mdp/resnet_utils.py New pure-torchvision helper: loads ResNet with FC replaced by Identity, caches normalization tensors, supports freeze flag — but inference never resizes inputs to the 224×224 the ImageNet weights expect.
source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/camera_cfg.py Adds ResNetSingleCameraObservationsCfg with a frozen ResNet18 obs group and correctly guards the history_length loop with isinstance(obs_group, ObsGroup).
source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/dexsuite_kuka_allegro_env_cfg.py Wires resnet_single_camera into scene and observation presets; scene alias is a direct reference rather than an independent copy which could cause aliasing surprises.
source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/agents/rsl_rl_resnet_cfg.py New RSL-RL runner config using a smaller MLP (256/128/64) matched to 512-dim ResNet18 features; hyperparameters look reasonable.
source/isaaclab/test/managers/test_resnet_model_preparation.py Good coverage of prepare_resnet_model in isolation; however the freeze=False gradient test only exercises resnet_utils directly and would miss the detach() bug in observations.py.
source/isaaclab_tasks/test/test_dexsuite_agent_presets.py Integration smoke-test that verifies preset coupling resolves the correct env and agent configs end-to-end.

Sequence Diagram

sequenceDiagram
    participant Env as ManagerBasedEnv
    participant IM as image_features
    participant RU as resnet_utils
    participant RN as ResNet18

    Note over IM: __init__ reads freeze from cfg.params
    IM->>RU: prepare_resnet_model(model_name, device, freeze)
    RU-->>IM: dict with model and inference callables
    IM->>RN: _load_model() — fc replaced with Identity
    RN-->>IM: loaded model

    Note over Env,IM: Per-step __call__
    Env->>IM: image_data at 64x64
    IM->>RU: _inference(model, image_data)
    Note over RU: permute then normalize — no resize to 224x224
    alt freeze=True
        RU->>RN: forward inside torch.no_grad()
    else freeze=False
        RU->>RN: forward with gradients
    end
    RN-->>RU: features (N, 512)
    RU-->>IM: features
    Note over IM: features.detach() always called — breaks freeze=False
    IM-->>Env: detached features on env device
Loading

Comments Outside Diff (1)

  1. source/isaaclab/isaaclab/envs/mdp/observations.py, line 551 (link)

    P1 freeze=False gradient flow silently broken by unconditional .detach()

    features.detach() is called unconditionally here, cutting the autograd graph regardless of the freeze parameter. When a caller sets freeze=False expecting gradients to flow through the backbone (the stated purpose in the CHANGELOG: "added an optional freeze parameter … to allow gradient flow through the backbone when needed"), the detach silently nullifies that. The unit test in test_resnet_model_preparation.py passes only because it calls resnet_utils._inference directly, bypassing this line.

Reviews (1): Last reviewed commit: "Address ResNet preset PR review comments" | Re-trigger Greptile

Comment on lines +84 to +105
def _inference(model, images: torch.Tensor) -> torch.Tensor:
"""Run inference on the ResNet model.

Args:
model: ResNet model with FC replaced by Identity.
images: Input tensor of shape ``(N, H, W, C)`` in ``[0, 255]`` range.

Returns:
Feature tensor of shape ``(N, feature_dim)``.
"""
device = _get_model_device(model)
image_proc = images.to(device)
image_proc = image_proc.permute(0, 3, 1, 2).float() / 255.0
mean, std = _get_normalization_tensors(device)
image_proc = (image_proc - mean) / std

if freeze:
with torch.no_grad():
features = model(image_proc)
else:
features = model(image_proc)
return features

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 No resize to 224×224 — ImageNet preprocessing mismatched at 64×64

The default camera resolution is 64×64 (BaseTiledCameraCfg.default = rgb64), but the inference function never resizes inputs before applying ImageNet normalization. Standard ResNet18 was trained on 224×224 crops; the mean/std values are calibrated for that resolution. While PyTorch's AdaptiveAvgPool2d(1,1) lets the model accept smaller spatial sizes, feature quality at 64×64 can be substantially degraded. Consider adding an explicit resize step or documenting the expected minimum resolution.

import torch.nn.functional as F
# before normalization
image_proc = F.interpolate(image_proc, size=(224, 224), mode="bilinear", align_corners=False)


default = KukaAllegroSceneCfg(num_envs=4096, env_spacing=3, replicate_physics=True)
single_camera = default.replace(base_camera=BaseTiledCameraCfg())
resnet_single_camera = single_camera

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 resnet_single_camera is a shared alias, not an independent preset

resnet_single_camera = single_camera assigns the same PresetCfg instance to both names. Any in-place mutation of single_camera after class definition will silently affect resnet_single_camera too. If you intend these to be independent (e.g., to later give the ResNet preset a 224×224 camera), use .replace() to create a distinct object.

Suggested change
resnet_single_camera = single_camera
resnet_single_camera = single_camera.replace()

@kellyguo11 kellyguo11 moved this to In review in Isaac Lab May 4, 2026
@kellyguo11 kellyguo11 moved this from In review to Backlog in Isaac Lab May 18, 2026

@isaaclab-review-bot isaaclab-review-bot Bot 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.

🤖 Isaac Lab Review Bot

Summary

Clean follow-up addressing all prior feedback. Ready to merge.

Changes Since Last Review (2531ee12e9d028)

The follow-up commit properly addresses the outstanding items:

  1. Image resize to ImageNet resolution — Added image_size=224 default parameter to _inference() with bilinear interpolation. Camera frames are now resized to match the ImageNet pretraining resolution, improving feature extraction quality for arbitrary camera resolutions.

  2. DRY observation group name — Introduced RESNET_FEATURES_OBS_GROUP = "resnet_features" constant in camera_cfg.py and referenced it from rsl_rl_resnet_cfg.py instead of hardcoding the string.

  3. Fixed scene preset inheritanceresnet_single_camera scene now correctly calls default.replace(base_camera=BaseTiledCameraCfg()) instead of aliasing single_camera, ensuring proper independence.

  4. Import fix — Changed from isaaclab.utils import configclass to from isaaclab.utils.configclass import configclass for consistency with the Python 3.13+ lazy-import pattern.

  5. Changelog fragments — Added proper changelog fragments (dexsuite-resnet-image-features.major.rst, dexsuite-resnet-camera-presets.rst) and cleaned up duplicate entries from CHANGELOG.rst.

  6. Additional test coverage — Added test_resnet18_resizes_camera_input_to_imagenet_resolution and test_resnet_single_camera_preset_supports_typed_selectors tests.

Implementation Verdict

Ship it — All review items addressed. The ResNet feature extraction integration is clean, well-tested, and follows Isaac Lab conventions.


Update (4b2b7ba): Reviewed incremental changes (2e9d0284b2b7ba).

Key changes:

  1. image_size default reverted to None_inference() now defaults to image_size=None (keep camera resolution) instead of 224. This is a behavioral change from the previous commit — users who want ImageNet resolution must explicitly pass image_size=224. The docstring was updated to reflect this.

  2. Test coverage updated — Renamed test to test_resnet18_keeps_camera_resolution_by_default() and added new test_resnet18_can_resize_camera_input_to_imagenet_resolution() to verify both behaviors.

  3. Import order fixes — Ray caster tests (test_ray_caster.py, test_ray_caster_patterns.py) now correctly place imports after AppLauncher initialization to avoid premature torch/warp loading.

  4. Preset CLI integration — Training script now uses setup_preset_cli() and fold_preset_tokens() from isaaclab_tasks.utils for cleaner preset argument handling.

  5. Changelog skip file — Added pbarejko-fix-torch-imports.skip changelog fragment (empty/skip marker for the import ordering fix).

Verdict: Changes look good. The image_size=None default is a reasonable choice — it preserves camera resolution by default and lets users opt-in to ImageNet scaling. Import order fixes are correct. Still LGTM

@gavrielstate

Copy link
Copy Markdown
Contributor

Note that ClawLabby ended up doing a bunch of updates to its PR branch without actually testing these. This PR should NOT be accepted until it actually validates that the changes work. In particular, the 224×224 resize is potentially a massive performance hit on the current branch.

@AntoineRichard

AntoineRichard commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Hi @ClawLabby — thank you so much for opening this PR, and for taking the time to contribute to Isaac Lab! 🙏

We're currently doing a big spring-clean of our pull request backlog, which had grown past 400 open PRs. Being honest: at that volume we stopped being able to give every contribution the attention it deserved, and this one is a case where we dropped the ball on getting back to you. That's on us, not on you.

Why this PR is being closed: Here is exactly what we found on this PR when we reviewed the backlog:

Opened 2026-04-30 (about 4 months ago)
Last commit on the branch 2026-05-19
Last activity from the author about 3 months ago
Target branch develop
Review status Never reviewed by a maintainer — nobody on the team got to it. Sorry about that.
Merge status Unknown
Size 12 commit(s), 12 file(s) changed, +469 / -49

It was picked up by the sweep because it has been open for about 4 months. It was then put in the "close" bucket because the author has been silent for about 3 months — which is the signal we used to tell apart pull requests that are still being worked on from ones that have genuinely been set aside.

We deliberately did not close pull requests that were approved and ready to land, or that were small and clearly still fixing a live bug — there were 27 of those, and we are merging them rather than closing them.

To be completely clear — this is not a judgement on the quality or the value of your work. It's purely about getting the queue down to a size where we can actually review things properly and give contributors real, timely feedback.

One thing worth knowing first: Isaac Lab 3.0 has since shipped. develop is now at
3.0.0 while main is still at 2.3.2, and 3.0 restructured a fair amount — quaternion
conventions, actuator collections, data access, and the Isaac Sim extension imports all
changed. The migration guide
is the place to start if you rebase.

It is also the honest reason a lot of these older pull requests stopped applying: the code
they touched moved or was rewritten underneath them.

If this change is still relevant, please reopen this PR or open a fresh one against develop. With the backlog down to a reviewable size, it should get a response a lot sooner than this one did.

Thanks again for contributing to Isaac Lab, and sorry for the long silence! 💚


🤖 This comment was drafted with AI assistance as part of a maintainer-led sweep of the Isaac Lab pull request backlog. A maintainer is behind this cleanup — but if this closure looks wrong, it may well be, so please push back and we'll take another look.

@github-project-automation github-project-automation Bot moved this from Backlog to Done in Isaac Lab Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request isaac-lab Related to Isaac Lab team

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants