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
81 changes: 81 additions & 0 deletions docs/how-tos/version-specific-prebuilt.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
Version-Specific Prebuilt Settings
===================================

When working with multiple collections that share the same variant but need
different versions of a package, you may want some versions to use prebuilt
wheels while others are built from source.

Version-specific prebuilt settings allow you to configure ``pre_built`` and
``wheel_server_url`` on a per-version basis within a variant, providing
fine-grained control over which package versions use prebuilt wheels.

Configuration
-------------

Add version-specific settings under the ``versions`` key within a variant:

.. code-block:: yaml

# overrides/settings/torchvision.yaml
variants:
tpu-ubi9:
# Default behavior for unlisted versions
pre_built: false

# Version-specific overrides
versions:
# Use prebuilt wheel for this version
"0.24.0.dev20250730":
pre_built: true
wheel_server_url: https://gitlab.com/api/v4/projects/12345/packages/pypi/simple

# Build from source for this version
"0.23.0":
pre_built: false

Available Settings
------------------

Within each version-specific block, you can configure:

``pre_built``
Boolean indicating whether to use prebuilt wheels for this version.

``wheel_server_url``
URL to download prebuilt wheels from for this version.

``env``
Environment variables specific to this version.

``annotations``
Version-specific annotations.

Precedence Rules
----------------

Version-specific settings override variant-wide settings. If both are defined,
environment variables are merged with version-specific values taking precedence
for conflicting keys.

Example Use Case
----------------

Consider two TPU collections using different ``torchvision`` versions:

**Global Collection** (``collections/accelerated/tpu-ubi9/requirements.txt``):

.. code-block:: text

torchvision==0.24.0.dev20250730

**Torch-2.8.0 Collection** (``collections/torch-2.8.0/tpu-ubi9/requirements.txt``):

.. code-block:: text

torchvision==0.23.0

With the configuration above:

- Global collection downloads prebuilt ``torchvision==0.24.0.dev20250730`` wheels
- Torch-2.8.0 collection builds ``torchvision==0.23.0`` from source
- Both use the same variant (``tpu-ubi9``) with different build methods
122 changes: 108 additions & 14 deletions src/fromager/bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,28 @@ def resolve_version(
return self._resolved_requirements[req_str]

pbi = self.ctx.package_build_info(req)
if pbi.pre_built:
source_url, resolved_version = self._resolve_prebuilt_with_history(
req=req,
req_type=req_type,
)

# Check if package has version-specific settings (any version-specific config)
variant_info = pbi._ps.variants.get(pbi._variant)
has_version_specific_prebuilt = (
Copy link
Member

Choose a reason for hiding this comment

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

Oh, this is going to be messier than I thought.

The _ps field really should be private to the PBI. We could at least wrap this logic in a method, but it's odd to need to know about the version-specific prebuilt flags before we resolve the version we're going to bootstrapping.

I don't remember why we have different logic for resolving versions of prebuilt wheels. How different is that logic from the other flow? Does it skip some steps or are there completely different steps?

variant_info and variant_info.versions and len(variant_info.versions) > 0
)

if pbi.pre_built or has_version_specific_prebuilt:
try:
source_url, resolved_version = self._resolve_prebuilt_with_history(
req=req,
req_type=req_type,
)
except Exception as e:
# Version-specific prebuilt resolution failed, fall back to source
logger.debug(
f"{req.name}: prebuilt resolution failed, falling back to source: {e}"
)
source_url, resolved_version = self._resolve_source_with_history(
req=req,
req_type=req_type,
)
else:
source_url, resolved_version = self._resolve_source_with_history(
req=req,
Expand Down Expand Up @@ -185,7 +202,8 @@ def bootstrap(self, req: Requirement, req_type: RequirementType) -> Version:

source_url_type = sources.get_source_type(self.ctx, req)

if pbi.pre_built:
# Use version-aware prebuilt check now that we have resolved_version
if pbi.is_pre_built(resolved_version):
wheel_filename, unpack_dir = self._download_prebuilt(
req=req,
req_type=req_type,
Expand Down Expand Up @@ -826,6 +844,19 @@ def _resolve_prebuilt_with_history(
req: Requirement,
req_type: RequirementType,
) -> tuple[str, Version]:
# Try version-specific resolution FIRST (highest priority)
# This allows version-specific wheel_server_url settings to override
# any cached resolutions from previous bootstraps or current graph
try:
wheel_url, resolved_version = (
self._resolve_prebuilt_with_version_specific_urls(req, req_type)
)
return (wheel_url, resolved_version)
except Exception:
# No version-specific settings matched, fall back to cached resolution
pass

# Fall back to cached resolution from graph
cached_resolution = self._resolve_from_graph(
req=req,
req_type=req_type,
Expand All @@ -835,15 +866,77 @@ def _resolve_prebuilt_with_history(
if cached_resolution and not req.url:
wheel_url, resolved_version = cached_resolution
logger.debug(f"resolved from previous bootstrap to {resolved_version}")
else:
servers = wheels.get_wheel_server_urls(
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
)
wheel_url, resolved_version = wheels.resolve_prebuilt_wheel(
ctx=self.ctx, req=req, wheel_server_urls=servers, req_type=req_type
)
return (wheel_url, resolved_version)

# Fall back to regular prebuilt wheel resolution (no version-specific or cached)
servers = wheels.get_wheel_server_urls(
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
)
wheel_url, resolved_version = wheels.resolve_prebuilt_wheel(
ctx=self.ctx, req=req, wheel_server_urls=servers, req_type=req_type
)
return (wheel_url, resolved_version)

def _resolve_prebuilt_with_version_specific_urls(
self,
req: Requirement,
req_type: RequirementType,
) -> tuple[str, Version]:
"""Resolve prebuilt wheel using version-specific wheel server URLs if configured."""
pbi = self.ctx.package_build_info(req)

# Check if there are version-specific settings
variant_info = pbi._ps.variants.get(pbi._variant)
if not variant_info or not variant_info.versions:
raise ValueError("No version-specific settings configured")

# Get the constraint for this package
constraint = self.ctx.constraints.get_constraint(req.name)

# Try to resolve using version-specific wheel server URLs
for version_str, version_settings in variant_info.versions.items():
# Only process versions that have both wheel_server_url and pre_built=True
if not (version_settings.wheel_server_url and version_settings.pre_built):
continue

# Only try this version if it satisfies the requirement specifier AND constraint
try:
version_obj = Version(version_str)

# Check requirement specifier (if present)
if req.specifier and version_obj not in req.specifier:
continue

# Check constraint (if present) - this is the version from constraints.txt
if constraint and version_obj not in constraint.specifier:
continue

except Exception:
continue # Skip invalid version strings

# Create a constraint for this specific version
version_req = Requirement(f"{req.name}=={version_str}")

try:
# Try to resolve this specific version from the version-specific server
wheel_url, resolved_version = wheels.resolve_prebuilt_wheel(
ctx=self.ctx,
req=version_req,
wheel_server_urls=[version_settings.wheel_server_url],
req_type=req_type,
)
logger.info(
f"{req.name}: using version-specific prebuilt wheel "
f"{resolved_version} from {version_settings.wheel_server_url}"
)
return (wheel_url, resolved_version)
except Exception:
# Version not found on this server, try next
continue

# No matching version-specific prebuilt settings found
raise ValueError("No matching version-specific prebuilt settings found")

def _resolve_from_graph(
self,
req: Requirement,
Expand Down Expand Up @@ -949,7 +1042,8 @@ def _add_to_graph(
req=req,
req_version=req_version,
download_url=download_url,
pre_built=pbi.pre_built,
# Use version-aware prebuilt check for dependency graph
pre_built=pbi.is_pre_built(req_version),
)
self.ctx.write_to_graph_to_file()

Expand Down
8 changes: 6 additions & 2 deletions src/fromager/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,14 @@ def _build(

logger.info("starting processing")
pbi = wkctx.package_build_info(req)
prebuilt = pbi.pre_built
# Use version-aware prebuilt check now that we have resolved_version
prebuilt = pbi.is_pre_built(resolved_version)

wheel_server_urls = wheels.get_wheel_server_urls(
wkctx, req, cache_wheel_server_url=cache_wheel_server_url
wkctx,
req,
cache_wheel_server_url=cache_wheel_server_url,
version=resolved_version,
)

# See if we can reuse an existing wheel.
Expand Down
Loading
Loading