Skip to content

Support lazy importing submodules in attach - #168

Closed
sjmonson wants to merge 3 commits into
scientific-python:mainfrom
sjmonson:feat/lazy_submodules
Closed

Support lazy importing submodules in attach #168
sjmonson wants to merge 3 commits into
scientific-python:mainfrom
sjmonson:feat/lazy_submodules

Conversation

@sjmonson

Copy link
Copy Markdown

Adds a lazy_submodules: bool argument to attach which substitutes importlib.import_module for the load function. Found this useful for bundling a bunch of functions/objects with their required extras. E.g.

pkg/extras/__init__.py

import lazy_loader as lazy

__getattr__, __dir__, __all__ = lazy.attach(
    __name__,
    submodules=["group_one", "group_two"],
    lazy_submodules=True,
)

pkg/extras/group_one.py

try:
    import numpy as np
except ImportError as e:
    raise ImportError("Install pkg[group_one] to use these features") from e

def add(x np.int64, y: np.int64) -> np.int64:
    ...

Signed-off-by: Samuel Monson <smonson@redhat.com>
Signed-off-by: Samuel Monson <smonson@redhat.com>
Signed-off-by: Samuel Monson <smonson@redhat.com>
mergify Bot pushed a commit to vllm-project/guidellm that referenced this pull request Jun 3, 2026
## TODO

- [ ] ~Pending upstream change: scientific-python/lazy-loader#168 upstream seems dead so the module has been pulled in-house.
- [ ] ~Support for soft extras (E.g. `perf` group)~ deferred
- [ ] ~Lazy load in `__main__.py` to improve CLI responsiveness~ deferred
- [ ] Docs cleanup (howto create a lazy module, do's and don'ts)
- [ ] Actually test vLLM python (loading of vLLM tested but not a full run)

## Summary

Lazy loads extras submodules in order to defer import errors to the time of use.

## Details

TODO

## Test Plan

Without vLLM:

1. Run `guidellm benchmark run --help` and observe no errors
2. Run `uv run guidellm benchmark run --backend vllm_python --model test` and observe error with helpful message

With vLLM:

1. Run `guidellm benchmark run --help` and observe no errors
2. Run `uv run guidellm benchmark run --backend vllm_python --model test ...` and observe successful benchmark
3. Run `tox -re test-e2e` and observe that tests pass (Previously they would fail when vLLM was installed due to load times)

## Related Issues

- Replaces #636 

---

- [x] "I certify that all code in this PR is my own, except as noted below."

## Use of AI

- [x] Includes AI-assisted code completion
- [ ] Includes code generated by an AI application
- [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`)



---

# git log

commit 19d8aad
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue May 19 17:49:25 2026 -0400

    Pull in custom lazy_load package as submodule
    
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit de50d31
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 19:50:10 2026 -0400

    Minor type fix
    
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 6066364
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue Mar 17 16:33:04 2026 -0400

    Switch to lazy-loading for extras packages
    
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 591a160
Author: Samuel Monson <smonson@redhat.com>
Date:   Wed Mar 18 16:58:33 2026 -0400

    Use AttributeError for failed optionals
    
    `torch` seems to break when it encounters lazy ImportErrors
    
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 79bcd7d
Author: Samuel Monson <smonson@redhat.com>
Date:   Wed May 20 17:42:54 2026 -0400

    LazyModule which loads module attributes individually
    
    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 812acf6
Author: Samuel Monson <smonson@redhat.com>
Date:   Wed May 20 17:50:37 2026 -0400

    Fix unit tests
    
    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 5a81cc2
Author: Samuel Monson <smonson@redhat.com>
Date:   Thu May 21 10:50:04 2026 -0400

    Revert "LazyModule which loads module attributes individually"
    
    This reverts commit e31862e.
    
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 8a00c01
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 20:05:32 2026 +0000

    Split lazy loading from functionality
    
    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit b309ac4
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 21:08:01 2026 +0000

    Cleanup tests after split
    
    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 7106bf8
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 22:35:47 2026 +0000

    Implement nested lazy loading
    
    Generated-by: claude-code Opus 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit fccdcd7
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue Jun 2 00:09:19 2026 +0000

    Switch attribute interface to a named tuple
    
    Generated-by: claude-code Opus 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 7dea6a0
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue Jun 2 00:47:11 2026 +0000

    Add type stubs
    
    Generated-by: claude-code Opus 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

---------

Generated-by: claude-code Opus 4.6
Generated-by: claude-code Sonnet 4.6
Signed-off-by: Samuel Monson <smonson@redhat.com>
SkiHatDuckie pushed a commit to SkiHatDuckie/guidellm that referenced this pull request Jun 8, 2026
## TODO

- [ ] ~Pending upstream change: scientific-python/lazy-loader#168 upstream seems dead so the module has been pulled in-house.
- [ ] ~Support for soft extras (E.g. `perf` group)~ deferred
- [ ] ~Lazy load in `__main__.py` to improve CLI responsiveness~ deferred
- [ ] Docs cleanup (howto create a lazy module, do's and don'ts)
- [ ] Actually test vLLM python (loading of vLLM tested but not a full run)

## Summary

Lazy loads extras submodules in order to defer import errors to the time of use.

## Details

TODO

## Test Plan

Without vLLM:

1. Run `guidellm benchmark run --help` and observe no errors
2. Run `uv run guidellm benchmark run --backend vllm_python --model test` and observe error with helpful message

With vLLM:

1. Run `guidellm benchmark run --help` and observe no errors
2. Run `uv run guidellm benchmark run --backend vllm_python --model test ...` and observe successful benchmark
3. Run `tox -re test-e2e` and observe that tests pass (Previously they would fail when vLLM was installed due to load times)

## Related Issues

- Replaces vllm-project#636

---

- [x] "I certify that all code in this PR is my own, except as noted below."

## Use of AI

- [x] Includes AI-assisted code completion
- [ ] Includes code generated by an AI application
- [ ] Includes AI-generated tests (NOTE: AI written tests should have a docstring that includes `## WRITTEN BY AI ##`)

---

# git log

commit 19d8aad
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue May 19 17:49:25 2026 -0400

    Pull in custom lazy_load package as submodule

    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit de50d31
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 19:50:10 2026 -0400

    Minor type fix

    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 6066364
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue Mar 17 16:33:04 2026 -0400

    Switch to lazy-loading for extras packages

    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 591a160
Author: Samuel Monson <smonson@redhat.com>
Date:   Wed Mar 18 16:58:33 2026 -0400

    Use AttributeError for failed optionals

    `torch` seems to break when it encounters lazy ImportErrors

    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 79bcd7d
Author: Samuel Monson <smonson@redhat.com>
Date:   Wed May 20 17:42:54 2026 -0400

    LazyModule which loads module attributes individually

    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 812acf6
Author: Samuel Monson <smonson@redhat.com>
Date:   Wed May 20 17:50:37 2026 -0400

    Fix unit tests

    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 5a81cc2
Author: Samuel Monson <smonson@redhat.com>
Date:   Thu May 21 10:50:04 2026 -0400

    Revert "LazyModule which loads module attributes individually"

    This reverts commit e31862e.

    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 8a00c01
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 20:05:32 2026 +0000

    Split lazy loading from functionality

    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit b309ac4
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 21:08:01 2026 +0000

    Cleanup tests after split

    Generated-by: claude-code Sonnet 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 7106bf8
Author: Samuel Monson <smonson@redhat.com>
Date:   Mon Jun 1 22:35:47 2026 +0000

    Implement nested lazy loading

    Generated-by: claude-code Opus 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit fccdcd7
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue Jun 2 00:09:19 2026 +0000

    Switch attribute interface to a named tuple

    Generated-by: claude-code Opus 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

commit 7dea6a0
Author: Samuel Monson <smonson@redhat.com>
Date:   Tue Jun 2 00:47:11 2026 +0000

    Add type stubs

    Generated-by: claude-code Opus 4.6
    Signed-off-by: Samuel Monson <smonson@redhat.com>

---------

Generated-by: claude-code Opus 4.6
Generated-by: claude-code Sonnet 4.6
Signed-off-by: Samuel Monson <smonson@redhat.com>
Signed-off-by: SkiHatDuckie <SkiHatDuckie@gmail.com>
@stefanv

stefanv commented Sep 2, 2026

Copy link
Copy Markdown
Member

Thanks for the contribution @sjmonson. I don't see a test that exercises the new behavior?

@sjmonson

sjmonson commented Sep 10, 2026

Copy link
Copy Markdown
Author

@stefanv Oh sorry I forgot about this PR. It actually turned out that while this idea worked in theory, in practice it is pretty fragile and packages like PyTorch can break it by trying to walk sys.modules.

I ended up solving this problem a different way by making a new version of attach which gets layered under a normal attach. You can see it in action here or here. I can make a new PR with those changes if you're interested; for now I'll just close this PR.

@sjmonson sjmonson closed this Sep 10, 2026
@stefanv

stefanv commented Sep 10, 2026

Copy link
Copy Markdown
Member

Thanks for the update, Samuel!

Can you explain a bit about what https://github.com/vllm-project/guidellm/blob/d07502b51b4c30d7acdc17ee8990c3899978cb08/src/guidellm/utils/lazy_loader.py#L163 does?

@sjmonson

Copy link
Copy Markdown
Author

Sure, basically the goal is to bring a series of imports in a attributes on the current module without importing the source module in advance. Since we don't know if each attribute we are importing is a attribute of the source module or a submodule of the source module we need to try a couple different things.

src/guidellm/extras/vision.py is a good example. We essentially want a module that looks like:

import imageio.v3 as iio
from PIL import Image as PILImage
from PIL.Image import Image

__all__ = ["Image", "PILImage", "iio"]

imageio.v3 is a module we bind to the iio attribute, PIL.Image is a module we bind to the PILImage attribute, and PIL.Image.Image is an attribute of the PIL.Image module that we bind to Image locally.

At the start of _attach_extras_attrs function we make a map which is <locally bound name>: (<source module>, <source attr name>). In the __getattr__ method after we check if the looked up attribute is in our bound name list, we attempt to import the source module, if that succeeds we try to getattr the source attr name and if that fails we try to import it as a module instead. This is here to ensure that attributes are cached in the module's __dict__ and IIRC ImportErrors are re-thown as AttributeErrors because PyTorch had some weird module walking behavior that would break if an ImportError occurred when accessing an attribute. Its been a bit so I can't remember the specifics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants