Skip to content
Open
Show file tree
Hide file tree
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
22 changes: 13 additions & 9 deletions docs/Flash_Deploy_Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ This guide walks through deploying a Flash application from local development to

## Prerequisites

- Python 3.10, 3.11, or 3.12
- Python 3.10, 3.11, 3.12, or 3.13
- `pip install runpod-flash`
- A Runpod account with API key ([get one here](https://docs.runpod.io/get-started/api-keys))

### Python version selection

Flash apps ship as a single tarball, so every resource in an app shares one Python version. The worker runtime defaults to 3.12 (the version torch is pre-installed for in the GPU base image). Select a different version in two ways:
Flash workers support Python 3.10, 3.11, 3.12, and 3.13. Native per-version images mean each interpreter has torch, numpy, and the worker runtime built directly for it — no side-by-side overhead, no cold-start tax for non-default versions.

- **Per-resource declaration**: set `python_version="3.11"` on any resource config — all resources in the same app must agree or leave it unset.
- **App-level override**: pass `--python-version 3.11` to `flash build` or `flash deploy`. The override wins over per-resource values that are unset and must match any that are set.
By default, `flash build` and `flash deploy` target the Python version you're running flash from. If you're on 3.11 locally, your deploy runs on 3.11; on 3.13, it runs on 3.13. The build prints the resolved version and its source on the first line of output.

| Version | Status | GPU cold-start | Notes |
|---------|--------|----------------|-------|
| 3.10 | Supported (EOL 2026-10-31) | +~7 GB alt-Python install | Consider migrating to 3.11 before EOL |
| 3.11 | Supported | +~7 GB alt-Python install | |
| 3.12 | Supported (default) | No overhead | Torch pre-installed in base image |
You can override the choice in two ways:

- **CLI flag:** `flash build --python-version 3.12` or `flash deploy --python-version 3.12`. Validated against the supported set.
- **Per-resource declaration:** set `python_version="3.12"` on any `LiveServerless`, `CpuLiveServerless`, `LiveLoadBalancer`, or `CpuLiveLoadBalancer` config. The build reconciles to the declared value (or raises if resources disagree).

For projects shared across a team, declare `python_version` explicitly on each resource config. That makes the deploy result identical regardless of who runs `flash build` and what Python they happen to have installed locally — useful when team members and CI run different interpreters.

Flash will refuse to build if your local Python is outside the supported set (e.g. 3.9 or 3.14). The error message tells you to pass `--python-version`, declare on a resource, or upgrade your interpreter.

The `runpod/flash:latest`, `runpod/flash-lb:latest`, `runpod/flash-cpu:latest`, and `runpod/flash-lb-cpu:latest` aliases all point at the 3.12 image variant — Dockerfiles or compose files that pin `:latest` will keep getting 3.12. Use the `:py3.X-{tag}` form for explicit version pinning.

## Quick Start

Expand Down
26 changes: 26 additions & 0 deletions src/runpod_flash/cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,28 @@ def _resolve_pip_python_version(manifest: dict) -> str | None:
return max(versions)


def _python_version_source(override: str | None, resources_dict: dict) -> str:
"""Return a human-readable source string for the resolved Python version.

Used at build time to surface where the resolved version came from:
explicit override, per-resource declaration, or local interpreter match.
"""
if override:
return "--python-version override"
declared = {
name: r["python_version"]
for name, r in resources_dict.items()
if r.get("python_version")
}
if declared:
# All declared values are identical at this point — reconcile would
# have raised otherwise — so report the lexicographically-first
# resource name for stability.
name = next(iter(sorted(declared)))
return f"declared on resource {name}"
return "matched local interpreter"


def run_build(
project_dir: Path,
app_name: str,
Expand Down Expand Up @@ -339,6 +361,10 @@ def run_build(
python_version=manifest_python_version_override,
)
manifest = manifest_builder.build()
console.print(
f"[dim]targeting Python {manifest_builder.python_version} "
f"({_python_version_source(manifest_python_version_override, manifest.get('resources', {}))})[/dim]"
)
manifest["source_fingerprint"] = compute_source_fingerprint(
project_dir, files
)
Expand Down
33 changes: 24 additions & 9 deletions src/runpod_flash/cli/commands/build_utils/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Any, Dict, List, Optional

from runpod_flash.core.resources.constants import (
DEFAULT_PYTHON_VERSION,
SUPPORTED_PYTHON_VERSIONS,
validate_python_version,
)

Expand Down Expand Up @@ -321,18 +321,23 @@ def _extract_config_properties(config: Dict[str, Any], resource_config) -> None:
def _reconcile_python_version(
self, resources_dict: Dict[str, Dict[str, Any]]
) -> str:
"""Pick one Python version for the app from per-resource declarations.
"""Pick one Python version for the app.

Flash apps ship as a single tarball, so every resource must target the
same Python ABI. Resolution order:
1. Explicit override passed to ManifestBuilder (validated)
2. Exactly one distinct ``python_version`` declared across resources
3. ``DEFAULT_PYTHON_VERSION`` when no resource declares one
3. The local interpreter (``sys.version_info``) — the user's
environment is the source of truth when nothing else is declared.

There is no fallback to a hardcoded default. A local interpreter
outside ``SUPPORTED_PYTHON_VERSIONS`` raises an actionable error.

Raises:
ValueError: When resources declare conflicting ``python_version``
values, or when the override conflicts with a resource's
explicit declaration.
ValueError: when resources declare conflicting ``python_version``
values; when the override conflicts with a resource's explicit
declaration; or when the local interpreter is unsupported and
no override or declaration was provided.
"""
per_resource: Dict[str, str] = {
name: r["python_version"]
Expand Down Expand Up @@ -366,14 +371,24 @@ def _reconcile_python_version(
raise ValueError(
"Flash apps require one python_version across all resources "
f"(found {sorted(distinct)}): {details}. Set python_version to the "
"same value on every resource, or omit it to use the default "
f"({DEFAULT_PYTHON_VERSION})."
"same value on every resource, pass --python-version, or run "
"flash from a single-version interpreter."
)

if distinct:
return validate_python_version(next(iter(distinct)))

return DEFAULT_PYTHON_VERSION
# Match the user's local interpreter — parity, not policy.
local = f"{sys.version_info.major}.{sys.version_info.minor}"
if local not in SUPPORTED_PYTHON_VERSIONS:
supported = ", ".join(SUPPORTED_PYTHON_VERSIONS)
raise ValueError(
f"Local Python {local} is not supported by Flash workers "
f"(supported: {supported}). Pass --python-version, declare "
f"python_version on a resource config, or run flash from a "
f"supported interpreter."
)
return local

def build(self) -> Dict[str, Any]:
"""Build the manifest dictionary.
Expand Down
8 changes: 4 additions & 4 deletions src/runpod_flash/cli/docs/flash-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ flash build [OPTIONS]
- `--no-deps`: Skip transitive dependencies during pip install (default: false)
- `--output, -o`: Custom archive name (default: artifact.tar.gz)
- `--exclude`: Comma-separated packages to exclude (e.g., 'torch,torchvision')
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, or `3.12`). Overrides per-resource `python_version`. Default: value declared on resource configs, or 3.12 if none set.
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, `3.12`, or `3.13`). Overrides per-resource `python_version` declarations and the local-interpreter default. By default, `flash build` targets the Python version you're running flash from.

To launch a local preview environment, use `flash deploy --preview` instead.

Expand Down Expand Up @@ -68,7 +68,7 @@ After `flash build` completes:
Flash automatically handles cross-platform builds, ensuring compatibility with Runpod's Linux x86_64 serverless infrastructure:

- **Automatic Platform Targeting**: Dependencies are always installed for Linux x86_64, regardless of your build platform (macOS, Windows, or Linux)
- **Python Version**: Targets Python 3.12 for wheel ABI selection regardless of local interpreter
- **Python Version**: Targets the resolved Python version (your local interpreter by default, or whatever `--python-version` / per-resource `python_version` selects) for wheel ABI selection
- **Binary Wheel Enforcement**: Only pre-built binary wheels are used, preventing platform-specific compilation issues

This means you can safely build on macOS ARM64, Windows, or any platform, and the deployment will work correctly on Runpod.
Expand Down Expand Up @@ -181,8 +181,8 @@ ls .flash/.build/my-project/
If a package doesn't have pre-built Linux x86_64 wheels:

1. **Install standard pip**: `python -m ensurepip --upgrade` -- standard pip has better manylinux compatibility than uv pip
2. **Check package availability**: Visit PyPI and verify the package has Linux wheels for Python 3.12
3. **Python 3.12**: All flash workers run Python 3.12. Ensure packages are available for this version.
2. **Check package availability**: Visit PyPI and verify the package has Linux wheels for your target Python version (`3.10`, `3.11`, `3.12`, or `3.13`)
3. **Match interpreter**: Flash builds default to your local Python version. If a wheel is missing for that version, either pick a different `--python-version` or upgrade/downgrade the package.
4. **Pure-Python packages**: These work regardless, as they don't require platform-specific builds

## Managing Deployment Size
Expand Down
2 changes: 1 addition & 1 deletion src/runpod_flash/cli/docs/flash-deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ flash deploy [OPTIONS]
- `--exclude`: Comma-separated packages to exclude (e.g., 'torch,torchvision')
- `--output, -o`: Custom archive name (default: artifact.tar.gz)
- `--preview`: Build and launch local preview environment instead of deploying
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, or `3.12`). Overrides per-resource `python_version`.
- `--python-version`: Target Python version for worker images (`3.10`, `3.11`, `3.12`, or `3.13`). Overrides per-resource `python_version` declarations and the local-interpreter default. By default, `flash deploy` targets the Python version you're running flash from.

## Examples

Expand Down
26 changes: 13 additions & 13 deletions src/runpod_flash/core/resources/constants.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import os

# Worker runtime Python versions. One tarball serves every resource in an app,
# so all resources must share a single Python version. GPU images ship 3.12
# with torch pre-installed; 3.10 and 3.11 are available via side-by-side
# install (~7 GB alt-Python overhead) in the same base image.
WORKER_PYTHON_VERSION: str = "3.12"
GPU_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12")
CPU_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12")

# Base image ships 3.12 with torch pre-installed; non-3.12 targets reinstall
# torch side-by-side for the selected interpreter.
GPU_BASE_IMAGE_PYTHON_VERSION: str = "3.12"
# Single source of truth for Python versions Flash supports end-to-end.
# Phase 1 of AE-2827 publishes native per-version worker images for each.
SUPPORTED_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12", "3.13")

# DEFAULT_PYTHON_VERSION drives the :latest tag aliases on Docker Hub
# (runpod/flash:latest -> runpod/flash:py3.12-latest). It is NOT a fallback
# the SDK reaches for — _reconcile_python_version uses sys.version_info
# when no override or per-resource declaration is set.
DEFAULT_PYTHON_VERSION: str = "3.12"

# Python versions that can run the flash SDK locally (for flash build, etc.)
SUPPORTED_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12")
# Per-image-type Python sets are now uniform — native per-version images
# cover GPU and CPU equally. Aliased to SUPPORTED_PYTHON_VERSIONS for any
# downstream callers that still reference the old names.
GPU_PYTHON_VERSIONS: tuple[str, ...] = SUPPORTED_PYTHON_VERSIONS
CPU_PYTHON_VERSIONS: tuple[str, ...] = SUPPORTED_PYTHON_VERSIONS


def local_python_version() -> str:
Expand Down
5 changes: 3 additions & 2 deletions src/runpod_flash/runtime/resource_provisioner.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ def create_resource_from_manifest(
deployment_kwargs = {"name": prefixed_name, "env": env}

# Use per-resource target_python_version (set by manifest builder based on
# resource type: GPU uses GPU_BASE_IMAGE_PYTHON_VERSION, CPU uses DEFAULT).
# Falls back to the caller-provided python_version for backward compatibility.
# the resource's declared python_version, or DEFAULT_PYTHON_VERSION when
# not specified). Falls back to the caller-provided python_version for
# backward compatibility.
effective_python_version = (
resource_data.get("target_python_version") or python_version
)
Expand Down
81 changes: 74 additions & 7 deletions tests/unit/cli/commands/build_utils/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,13 +944,13 @@ def test_manifest_includes_python_version():
)
]

builder = ManifestBuilder("test_app", functions)
# Explicit override avoids depending on the runner's local interpreter,
# which is the new resolution default after AE-2827.
builder = ManifestBuilder("test_app", functions, python_version="3.12")
manifest = builder.build()

assert "python_version" in manifest
from runpod_flash.core.resources.constants import DEFAULT_PYTHON_VERSION

assert manifest["python_version"] == DEFAULT_PYTHON_VERSION
assert manifest["python_version"] == "3.12"


def test_manifest_uses_explicit_python_version():
Expand Down Expand Up @@ -1000,13 +1000,21 @@ class TestReconcilePythonVersion:
def _builder(self, python_version: Optional[str] = None) -> ManifestBuilder:
return ManifestBuilder("test_app", [], python_version=python_version)

def test_no_resources_declare_version_uses_default(self):
from runpod_flash.core.resources.constants import DEFAULT_PYTHON_VERSION
def test_no_resources_no_override_uses_local_interpreter(self, monkeypatch):
"""With no override and no declaration, reconcile reads sys.version_info."""

class _StubVersionInfo:
pass

info = _StubVersionInfo()
info.major = 3
info.minor = 11
monkeypatch.setattr(sys, "version_info", info)

resolved = self._builder()._reconcile_python_version(
_make_resources_dict(gpu=None, cpu=None)
)
assert resolved == DEFAULT_PYTHON_VERSION
assert resolved == "3.11"

def test_single_declared_version_wins(self):
resolved = self._builder()._reconcile_python_version(
Expand Down Expand Up @@ -1053,3 +1061,62 @@ def test_unsupported_override_raises(self):
def test_unsupported_resource_version_raises(self):
with pytest.raises(ValueError, match="not supported"):
self._builder()._reconcile_python_version(_make_resources_dict(gpu="3.8"))


class TestReconcileLocalInterpreter:
"""Tests for AE-2827 match-local default in _reconcile_python_version."""

def _builder(self, python_version: Optional[str] = None) -> ManifestBuilder:
return ManifestBuilder("test_app", [], python_version=python_version)

def _stub_version_info(self, monkeypatch, major: int, minor: int):
class _StubVersionInfo:
pass

info = _StubVersionInfo()
info.major = major
info.minor = minor
monkeypatch.setattr(sys, "version_info", info)

@pytest.mark.parametrize(
"major,minor,expected",
[
(3, 10, "3.10"),
(3, 11, "3.11"),
(3, 12, "3.12"),
(3, 13, "3.13"),
],
)
def test_resolves_to_local_when_no_override_or_declaration(
self, monkeypatch, major, minor, expected
):
self._stub_version_info(monkeypatch, major, minor)
resolved = self._builder()._reconcile_python_version(
_make_resources_dict(gpu=None, cpu=None)
)
assert resolved == expected

@pytest.mark.parametrize("major,minor", [(3, 9), (3, 14)])
def test_raises_when_local_is_unsupported(self, monkeypatch, major, minor):
self._stub_version_info(monkeypatch, major, minor)
with pytest.raises(ValueError, match="Local Python") as excinfo:
self._builder()._reconcile_python_version(
_make_resources_dict(gpu=None, cpu=None)
)
message = str(excinfo.value)
assert f"{major}.{minor}" in message
assert "--python-version" in message

def test_local_does_not_shadow_override(self, monkeypatch):
self._stub_version_info(monkeypatch, 3, 10)
resolved = self._builder("3.12")._reconcile_python_version(
_make_resources_dict(gpu=None, cpu=None)
)
assert resolved == "3.12"

def test_local_does_not_shadow_declaration(self, monkeypatch):
self._stub_version_info(monkeypatch, 3, 13)
resolved = self._builder()._reconcile_python_version(
_make_resources_dict(gpu="3.11", cpu=None)
)
assert resolved == "3.11"
23 changes: 15 additions & 8 deletions tests/unit/core/resources/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from runpod_flash.core.resources.constants import (
CPU_PYTHON_VERSIONS,
DEFAULT_PYTHON_VERSION,
GPU_BASE_IMAGE_PYTHON_VERSION,
GPU_PYTHON_VERSIONS,
SUPPORTED_PYTHON_VERSIONS,
get_image_name,
Expand All @@ -19,19 +18,27 @@

class TestSupportedPythonVersions:
def test_supported_versions(self):
assert SUPPORTED_PYTHON_VERSIONS == ("3.10", "3.11", "3.12")
assert SUPPORTED_PYTHON_VERSIONS == ("3.10", "3.11", "3.12", "3.13")

def test_gpu_python_versions(self):
assert GPU_PYTHON_VERSIONS == ("3.10", "3.11", "3.12")
assert GPU_PYTHON_VERSIONS == ("3.10", "3.11", "3.12", "3.13")

def test_cpu_python_versions(self):
assert CPU_PYTHON_VERSIONS == ("3.10", "3.11", "3.12")
assert CPU_PYTHON_VERSIONS == ("3.10", "3.11", "3.12", "3.13")

def test_default_python_version_is_3_12(self):
assert DEFAULT_PYTHON_VERSION == "3.12"

def test_gpu_base_image_python_version(self):
assert GPU_BASE_IMAGE_PYTHON_VERSION == "3.12"
def test_supported_python_versions_contains_310_through_313(self):
from runpod_flash.core.resources.constants import SUPPORTED_PYTHON_VERSIONS

assert SUPPORTED_PYTHON_VERSIONS == ("3.10", "3.11", "3.12", "3.13")

def test_default_python_version_unchanged_for_latest_alias(self):
"""DEFAULT_PYTHON_VERSION drives the :latest tag alias, not SDK fallback."""
from runpod_flash.core.resources.constants import DEFAULT_PYTHON_VERSION

assert DEFAULT_PYTHON_VERSION == "3.12"


class TestGetImageName:
Expand Down Expand Up @@ -82,7 +89,7 @@ def test_invalid_image_type_raises(self):

def test_invalid_python_version_raises(self):
with pytest.raises(ValueError, match="not supported"):
get_image_name("gpu", "3.13")
get_image_name("gpu", "3.14")

def test_custom_tag(self):
assert get_image_name("gpu", "3.12", tag="v2.0") == "runpod/flash:py3.12-v2.0"
Expand Down Expand Up @@ -128,7 +135,7 @@ def test_valid_versions(self):

def test_invalid_version_raises(self):
with pytest.raises(ValueError, match="not supported"):
validate_python_version("3.13")
validate_python_version("3.14")

def test_old_version_raises(self):
with pytest.raises(ValueError, match="not supported"):
Expand Down
Loading
Loading