dynamo/container/render.py

233 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
import argparse
import re
from pathlib import Path
import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined
_VALID_ARCHS = {"amd64", "arm64"}
def parse_platform(platform_str: str) -> str:
"""Normalize a --platform value to the template variable used by Jinja2.
Accepts Docker-style values (linux/amd64, linux/arm64) or short form (amd64,
arm64), and comma-separated lists for multi-arch (linux/amd64,linux/arm64).
Returns one of: 'amd64', 'arm64', or 'multi'.
Raises ValueError for unrecognized architecture values.
"""
parts = [p.strip() for p in platform_str.split(",")]
archs = [p.split("/")[-1] for p in parts]
for arch in archs:
if arch not in _VALID_ARCHS:
raise ValueError(
f"Unrecognized architecture '{arch}' in --platform '{platform_str}'. "
f"Valid architectures: {', '.join(sorted(_VALID_ARCHS))}"
)
if len(archs) > 1:
return "multi"
return archs[0]
def parse_args():
parser = argparse.ArgumentParser(
description="Renders dynamo Dockerfiles from templates"
)
parser.add_argument(
"--framework",
type=str,
default="vllm",
choices=["dynamo", "vllm", "sglang", "trtllm"],
help="Dockerfile framework to use",
)
parser.add_argument(
"--device",
type=str,
default="cuda",
choices=["cuda", "xpu", "cpu"],
help="Dockerfile device to use",
)
parser.add_argument(
"--target",
type=str,
default="runtime",
help="Dockerfile target to use. Non-exhaustive examples: [runtime, dev, local-dev]",
)
parser.add_argument(
"--platform",
type=str,
default="linux/amd64",
help=(
"Target platform(s), Docker-style. Examples:\n"
" linux/amd64 single-arch amd64 build\n"
" linux/arm64 single-arch arm64 build\n"
" linux/amd64,linux/arm64 multi-arch build; the rendered Dockerfile uses\n"
" Docker BuildX TARGETARCH directly (set per platform\n"
" by: docker buildx build --platform linux/amd64,linux/arm64)"
),
)
parser.add_argument(
"--cuda-version",
type=str,
default="12.9",
choices=["12.9", "13.0", "13.1"],
help="CUDA version to use. [12.9 or 13.0 for vllm and sglang, 13.1 for trtllm]",
)
parser.add_argument("--make-efa", action="store_true", help="Enable AWS EFA")
parser.add_argument(
"--output-short-filename",
action="store_true",
help="Output filename is rendered.Dockerfile instead of <framework>-<target>-cuda<cuda_version>-<arch>-rendered.Dockerfile",
)
parser.add_argument(
"--show-result",
action="store_true",
help="Prints the rendered Dockerfile to stdout.",
)
args = parser.parse_args()
return args
def validate_args(args):
valid_inputs = {
"vllm": {
"device": ["cuda", "xpu", "cpu"],
"target": [
"runtime",
"dev",
"local-dev",
"framework",
"wheel_builder",
"base",
],
"cuda_version": ["12.9", "13.0"],
},
"trtllm": {
"device": ["cuda"],
"target": [
"runtime",
"dev",
"local-dev",
"framework",
"wheel_builder",
"base",
],
"cuda_version": ["13.1"],
},
"sglang": {
"device": ["cuda"],
"target": [
"runtime",
"dev",
"local-dev",
"wheel_builder",
"base",
],
"cuda_version": ["12.9", "13.0"],
},
"dynamo": {
"device": ["cuda"],
"target": [
"runtime",
"dev",
"local-dev",
"frontend",
"wheel_builder",
"base",
],
"cuda_version": ["12.9", "13.0"],
},
}
if args.framework in valid_inputs:
if (
args.target in valid_inputs[args.framework]["target"]
and args.cuda_version in valid_inputs[args.framework]["cuda_version"]
and args.device in valid_inputs[args.framework]["device"]
):
return
raise ValueError(
f"Invalid input combination: [framework={args.framework},target={args.target},cuda_version={args.cuda_version},device={args.device}]"
)
raise ValueError(
f"Invalid input combination: [framework={args.framework},target={args.target},cuda_version={args.cuda_version},device={args.device}]"
)
def render(args, context, script_dir):
env = Environment(
loader=FileSystemLoader(script_dir),
trim_blocks=False,
lstrip_blocks=True,
undefined=StrictUndefined, # Raise an error if a variable in the template is not provided in the context
)
template = env.get_template("Dockerfile.template")
rendered = template.render(
context=context,
framework=args.framework,
device=args.device,
target=args.target,
platform=args.platform, # normalized: 'amd64', 'arm64', or 'multi'
cuda_version=args.cuda_version,
make_efa=args.make_efa,
)
# Replace all instances of 3+ newlines with 2 newlines
cleaned = re.sub(r"\n{3,}", "\n\n", rendered)
if args.output_short_filename:
filename = "rendered.Dockerfile"
else:
filename = f"{args.framework}-{args.target}-{args.device}{args.cuda_version}-{args.platform}-rendered.Dockerfile"
with open(f"{script_dir}/{filename}", "w") as f:
f.write(cleaned)
if args.show_result:
print("##############")
print("# Dockerfile #")
print("##############")
print(cleaned)
print("##############")
print(f"INFO: Generated Dockerfile written to {script_dir}/{filename}")
return
def main():
args = parse_args()
# Normalize platform to template variable ('amd64', 'arm64', or 'multi')
# and store it back so render() and validate_args() both see the normalized form.
args.platform = parse_platform(args.platform)
validate_args(args)
# Clear cuda version for non-cuda device
if args.device != "cuda":
args.cuda_version = ""
script_dir = Path(__file__).parent
with open(f"{script_dir}/context.yaml", "r") as f:
context = yaml.safe_load(f)
render(args, context, script_dir)
if args.target == "local-dev":
print(
"INFO: Remember to add --build-arg values for USER_UID and USER_GID when building a local-dev image!"
)
print(
" Recommendation: --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)"
)
if __name__ == "__main__":
main()