Skip to content

feat: bundle local (non-pip) modules for serverless endpoints#352

Open
deanq wants to merge 15 commits into
mainfrom
deanquinanola/sls-360-local-module-bundling
Open

feat: bundle local (non-pip) modules for serverless endpoints#352
deanq wants to merge 15 commits into
mainfrom
deanquinanola/sls-360-local-module-bundling

Conversation

@deanq

@deanq deanq commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Flash could not reliably include local (non-pip) Python modules an endpoint imports — a sibling utils.py, or a helpers/ package next to the endpoint, imported via import utils / from helpers import x. This adds first-class support across both code paths that ship user code to a worker.

Resolves SLS-360.

  • Live serverless (@Endpoint(...).run(...), flash dev LB dispatch): the endpoint's transitive local-import closure is now shipped inline alongside the function source.
  • Build/deploy (flash build / flash deploy): local modules an endpoint imports are force-included in the artifact even when the ignore filter would drop them, and the build fails loudly (clean error, not a traceback) if an endpoint's local import can't be resolved.

What changed

  • FunctionRequest.modules: dict[str, str] — additive protocol field (POSIX relative path → source text), default empty. Backward compatible: old workers ignore it, old SDKs send nothing. Shared contract with the worker repo — see the companion worker PR: feat: SLS-360 import shipped local modules before executing user code runpod-workers/flash#100.
  • stubs/local_modules.py — a shared, pure resolve_local_modules(...) that walks the transitive local-import closure (absolute, relative, and importlib.import_module("literal") imports, at module level and inside function bodies), classifies local vs stdlib/installed, pulls in package __init__.py, terminates on cycles, warns on non-literal dynamic imports, and raises LocalModuleResolutionError on unresolved relative imports / out-of-root files.
  • Live sendersstubs/live_serverless.py and stubs/load_balancer_sls.py populate modules from the resolved closure, with an 8 MiB inline cap raising LocalModulePayloadTooLargeError (points users to flash deploy for larger local dependencies).
  • Worker side (this repo)runtime/module_loader.py::materialized_modules writes shipped modules to a temp dir on sys.path and cleans up; wired into runtime/lb_handler.py's /execute so the temp dir stays live through the function call (not just exec).
  • Build pathaugment_with_local_modules in cli/commands/build.py force-includes resolved local files past the ignore filter. Strictness is scoped to endpoint files (@Endpoint/@remote): an endpoint with an unresolved local import fails the build loudly; an incidental non-endpoint file that fails resolution is warned and skipped (no regression on unrelated files).

Test plan

  • Unit: resolver (transitive closure, relative/dynamic/star imports, cycles, package inits, stdlib/external classification), materializer (writes files, restores sys.path on exception, no-op when empty), both live senders populate modules, LB /execute def-now/call-later import regression, build force-include + endpoint-only-strict A/B.
  • make quality-check green (86%+ coverage).
  • Deploy path verified end-to-end with real flash build: an ignored test_*.py sibling imported by an endpoint is force-included into artifact.tar.gz; an endpoint with an unresolvable relative import fails the build with a clean error (exit 1, no traceback).

Coordinated release

The worker-side materializer requires the companion worker PR (runpod-workers/flash#100) to be released for the live inline-shipping path to work end-to-end. The deploy path is independent (bundled files sit on the worker filesystem regardless of image version). Live-path E2E against real endpoints is a gating step once the worker image is rebuilt/published.

Follow-ups (non-blocking)

  • Live path silently omits parent-directory absolute imports (only relative imports fail loudly) — documented constraint; endpoint should sit at/above its local deps or use flash deploy.
  • sys.path materialization assumes one invocation at a time per worker; isolate per-invocation if concurrent execution is enabled.
  • Endpoint detection in the build path handles decorator forms; the ep = Endpoint(...) assignment form is not yet detected (fails safe toward warn).

@capy-ai

capy-ai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@promptless

promptless Bot commented Jul 6, 2026

Copy link
Copy Markdown

Promptless prepared a documentation update related to this change.

Triggered by PR #352: feat: bundle local (non-pip) modules for serverless endpoints

Documents Flash's new first-class support for bundling local (non-pip) Python modules that endpoints import: a new "Import local modules" section in the Flash endpoints guide (supported import forms, transitive resolution, the 8 MiB live-execution cap, and the parent-directory absolute-import constraint), a force-include note in the build CLI reference, and two troubleshooting entries for the new resolution and payload-size errors.

Review: Document Flash local (non-pip) module bundling for endpoints

@deanq deanq force-pushed the deanquinanola/sls-360-local-module-bundling branch from 724bc98 to 0c8c90c Compare July 8, 2026 15:14
@deanq deanq requested a review from Copilot July 11, 2026 06:23

Copilot AI 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.

Pull request overview

Adds first-class support for bundling/shipping local (non-pip) Python modules that serverless endpoints import, across both live execution (inline module shipping) and build/deploy (force-include local imports in artifacts), with new protocol support via FunctionRequest.modules.

Changes:

  • Added a shared local-import resolver (resolve_local_modules) to compute a transitive closure of local dependencies for endpoint code.
  • Live execution paths now populate modules (subject to an 8 MiB cap) and worker runtime materializes shipped modules onto sys.path for the duration of the function call.
  • Build path now force-includes locally imported modules that would otherwise be ignored, and fails builds for unresolved endpoint-local imports.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/test_stub_live_serverless.py Verifies live stub request includes sibling local modules.
tests/unit/test_load_balancer_sls_stub.py Verifies LB stub request includes sibling local modules.
tests/unit/stubs/test_local_modules_resolve.py Unit tests for transitive local module resolution behavior and error cases.
tests/unit/stubs/test_local_modules_classify.py Unit tests for stdlib detection, local path resolution, and size cap constant.
tests/unit/stubs/test_live_serverless_modules.py Tests build_modules_map behavior and size cap enforcement.
tests/unit/runtime/test_module_loader.py Tests temp materialization behavior and sys.path restoration.
tests/unit/runtime/test_lb_handler_extended.py Regression test ensuring shipped modules remain importable during function execution.
tests/unit/protos/test_remote_execution.py Verifies FunctionRequest.modules defaults and round-trips.
tests/unit/core/test_exceptions.py Tests new exception types’ message behavior.
tests/unit/cli/commands/test_build_local_modules.py Tests build-time force-include and endpoint-only strictness behavior.
tests/unit/_live_serverless_prepare_request_sibling.py Sibling module fixture for live-serverless stub tests.
tests/unit/_lb_prepare_request_sibling.py Sibling module fixture for LB stub tests.
src/runpod_flash/stubs/local_modules.py New AST-based resolver for local module import closure and warnings/errors.
src/runpod_flash/stubs/load_balancer_sls.py Populates modules in LB request using build_modules_map.
src/runpod_flash/stubs/live_serverless.py Adds build_modules_map and populates modules in live request with cap enforcement.
src/runpod_flash/runtime/module_loader.py New temp-dir module materializer that prepends to sys.path during execution.
src/runpod_flash/runtime/lb_handler.py Keeps module materialization active through exec + function call/await.
src/runpod_flash/protos/remote_execution.py Adds FunctionRequest.modules protocol field with default empty dict.
src/runpod_flash/core/exceptions.py Adds LocalModuleResolutionError and LocalModulePayloadTooLargeError.
src/runpod_flash/cli/commands/build.py Adds endpoint detection and build-time augmentation to force-include local imports.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/runpod_flash/runtime/module_loader.py Outdated
Comment thread src/runpod_flash/cli/commands/build.py
deanq added a commit to runpod-workers/flash that referenced this pull request Jul 11, 2026
Worker uses runpod_flash.runtime.module_loader, which is not in a published
runpod-flash yet. Temporary git pin so CI installs a flash with the module.
Revert to a released constraint once runpod/flash#352 ships.
@deanq deanq requested review from KAJdev and jhcipar July 13, 2026 07:19
- runtime/module_loader: reject module paths that resolve outside the
  temp dir (modules is untrusted request-body input); restructure
  cleanup so the temp dir is not leaked when the guard raises mid-loop
- cli/build: normalize a raw UnicodeDecodeError from an endpoint's
  dependency file into LocalModuleResolutionError so run_build() emits
  a clean error instead of a traceback
- add tests for path-escape rejection (incl. mid-loop + disk cleanup)
  and the wrapped endpoint dependency-decode error

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Added 1 comment

Comment thread src/runpod_flash/stubs/local_modules.py Outdated

@KAJdev KAJdev 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.

for the most part I think this is okay, though I dont really like that we are re-introducing the AST-walking pattern. I think it's okay just because its far more difficult than trying to do it via inspect and its probably more efficient than sending the entire tree every request. Logistically though, _defines_endpoint should probably live as a step within the scanner somewhere instead of duplicating work that we already do via inspect.

I think what I disagree with is force including files that are ignored by .flashignore, which is a very deliberate user signal that we shouldn't just overlook.

deanq added a commit to runpod-workers/flash that referenced this pull request Jul 14, 2026
…#100)

* feat: import shipped local modules before executing function code

* fix(review): address Copilot feedback for #100

- Use getattr(request, "modules", {}) for older-request compatibility
- Rename test's shipped module to a unique name to avoid collision with
  installed/cached modules

* build(deps): pin runpod-flash to flash SLS-360 branch for CI

Worker uses runpod_flash.runtime.module_loader, which is not in a published
runpod-flash yet. Temporary git pin so CI installs a flash with the module.
Revert to a released constraint once runpod/flash#352 ships.
Address PR #352 review feedback.

- build: replace force-inclusion of ignore-dropped modules with a loud
  build failure. validate_local_module_imports names each excluded file
  and its importer and tells the user to remove the ignore pattern or
  stop importing it, rather than silently overriding a deliberate
  .gitignore / built-in exclusion. _defines_endpoint stays AST-based
  (pre-copy) and now only scopes the unresolvable-import strictness.

- local_modules: resolve `from pkg import name` submodule candidates
  (pkg/name.py), not just pkg/__init__.py, so package submodules ship and
  the worker no longer hits ModuleNotFoundError. A name that is only a
  re-exported symbol does not back a file and is skipped, never errored.
@deanq

deanq commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

Addressed in 7272691.

Force-including ignored files — removed entirely. augment_with_local_modules is now validate_local_module_imports: if shipped code imports a local module the ignore rules dropped, the build fails with an actionable error naming the excluded file and its importer, telling the user to remove the ignore pattern or stop importing it. No more silently overriding the exclusion.

One correction on the framing: .flashignore was removed in v1.4 — load_ignore_patterns only warns if one still exists. The exclusions actually in play are .gitignore + the built-in defaults, so the error message names those rather than .flashignore.

_defines_endpoint — I kept it AST-based in build.py rather than folding it into the scanner. validate_local_module_imports runs pre-copy on source files and can't import them (deps aren't materialized in the build dir yet), while RuntimeScanner discovers endpoints by importing the copied build dir and reading __remote_config__ — different phase, different mechanism. Merging them would couple file-selection to post-copy import. It's now only used to scope the unresolvable-import strictness (endpoint = hard fail, incidental = warn); the new ignore-conflict check applies to all shipped files and needs no endpoint detection at all. Happy to do the unification as a follow-up if you still want it.

@deanq deanq requested a review from KAJdev July 14, 2026 06:02
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.

3 participants