"examples/vscode:/vscode.git/clone" did not exist on "da783ce0d36c67e9ae780f2111bb2080551b7f2e"
Unverified Commit ac020629 authored by Dillon Cullinan's avatar Dillon Cullinan Committed by GitHub
Browse files

feat: Dockerfile templating (#5633)


Signed-off-by: default avatarDillon Cullinan <dcullinan@nvidia.com>
parent 5755a8de
......@@ -74,7 +74,8 @@ docker compose -f deploy/docker-compose.yml up -d
We have public images available on [NGC Catalog](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/ai-dynamo/collections/ai-dynamo/artifacts). If you'd like to build your own container from source:
```bash
./container/build.sh --framework VLLM
python container/render.py --framework vllm --short-output
docker build -f container/rendered.Dockerfile -t dynamo:latest-vllm .
```
### Run container
......
......@@ -42,8 +42,9 @@ See the [support matrix](../../../docs/reference/support-matrix.md) for version
The pip wheel is built through a Docker build process:
```bash
# Build the Docker image with KVBM enabled (from the dynamo repo root)
./container/build.sh --framework none --enable-kvbm --tag local-kvbm
# Render and build the Docker image with KVBM enabled (from the dynamo repo root)
python container/render.py --framework=dynamo --target==runtime --short-output
docker build --build-arg ENABLE_KVBM="true" -f container/rendered.Dockerfile .
```
Once built, you can either:
......
......@@ -50,7 +50,7 @@ register_llm(
> **Requires GPU node**: The frontend must run on a node with GPU access. During media processing, decoded tensors are written to GPU memory via NIXL, which requires `libcuda.so.1` to be available. Running the frontend on a CPU-only node will fail with something like: `Failed to initialize required backends: [UCX: No UCX plugin found]`.
> [!WARNING]
> **Video decoding**: Video decoding needs to be enabled via the `dynamo-llm/media-ffmpeg` rust feature. The following ffmpeg dynamic libraries must be available on the system: `libavcodec`, `libavdevice`, `libavfilter`, `libavformat`, `libswresample`, `libswscale`. These are available in dynamo containers built with `container/build.sh --enable-media-ffmpeg ...`
> **Video decoding**: Video decoding needs to be enabled via the `dynamo-llm/media-ffmpeg` rust feature. The following ffmpeg dynamic libraries must be available on the system: `libavcodec`, `libavdevice`, `libavfilter`, `libavformat`, `libswresample`, `libswscale`. These are available in dynamo dockerfiles rendered with `enable_media_ffmpeg` set to true in `container/context.yaml`.
## Image decoding options
......
......@@ -129,7 +129,8 @@ then this test will be included in the nightly run as well.
```
- Run in container:
```bash
./container/build.sh --framework <backend>
python container/render.py --framework=${FRAMEWORK} --target=runtime --short-output
docker build -t dynamo:${FRAMEWORK}-latest -f container/rendered.Dockerfile .
./container/run.sh --mount-workspace -it -- pytest
./container/run.sh --mount-workspace -it -- pytest -m [optional markers]
```
......
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import shutil
import subprocess
import tarfile
import tempfile
import urllib.request
from pathlib import Path
import pytest
pytestmark = [pytest.mark.unit, pytest.mark.pre_merge, pytest.mark.gpu_0]
@pytest.fixture(scope="module")
def dynamo_repo_dir():
"""
Create a temporary copy of the dynamo repository for isolated testing.
Attempts to download the repository from GitHub at the current HEAD commit.
Falls back to creating a local archive if download fails.
"""
# Get the repository root (3 levels up from this test file)
repo_root = Path(__file__).parent.parent.parent
# Get commit hash from environment variable or git
commit_hash = os.environ.get("DYNAMO_COMMIT_SHA")
if commit_hash:
print(f"Using commit from DYNAMO_COMMIT_SHA: {commit_hash}")
else:
try:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=repo_root,
capture_output=True,
text=True,
timeout=10,
check=True,
)
commit_hash = result.stdout.strip()
print(f"Using commit from git HEAD: {commit_hash}")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
raise RuntimeError(f"Failed to get git HEAD commit: {e}")
with tempfile.TemporaryDirectory() as tmpdir:
tar_path = Path(tmpdir) / "dynamo.tar.gz"
# Try to download from GitHub
github_url = f"https://github.com/ai-dynamo/dynamo/archive/{commit_hash}.tar.gz"
downloaded = False
try:
print(f"Attempting to download repository from {github_url}")
with urllib.request.urlopen(github_url, timeout=30) as response:
with open(tar_path, "wb") as f:
shutil.copyfileobj(response, f)
downloaded = True
print("Successfully downloaded repository archive from GitHub")
except Exception as e:
print(f"Failed to download from GitHub: {e}")
print("Falling back to local git archive")
# Fallback to local git archive
if not downloaded:
try:
subprocess.run(
[
"git",
"archive",
"HEAD",
"--format=tar.gz",
f"--output={tar_path}",
],
cwd=repo_root,
check=True,
timeout=60,
)
print("Successfully created local git archive")
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
raise RuntimeError(f"Failed to create local git archive: {e}")
# Extract the archive
extract_dir = Path(tmpdir) / "extracted"
extract_dir.mkdir()
with tarfile.open(tar_path, "r:gz") as tar:
tar.extractall(extract_dir, filter="data")
# GitHub archives extract to dynamo-{commit}/ subdirectory
# Local git archives extract directly
extracted_contents = list(extract_dir.iterdir())
if len(extracted_contents) == 1 and extracted_contents[0].is_dir():
# GitHub format: single subdirectory
repo_dir = extracted_contents[0]
else:
# Local git archive format: files directly in extract_dir
repo_dir = extract_dir
print(f"Repository extracted to: {repo_dir}")
yield str(repo_dir)
@pytest.fixture
def temp_wheel_dir():
"""Create a temporary directory for wheel files"""
with tempfile.TemporaryDirectory() as tmpdir:
# Create a dummy wheel file so the directory is valid
wheel_file = Path(tmpdir) / "tensorrt_llm-1.0.0-py3-none-any.whl"
wheel_file.touch()
yield tmpdir
@pytest.fixture
def build_script_path(dynamo_repo_dir):
"""Get the path to the build.sh script from the temporary repository"""
build_sh = Path(dynamo_repo_dir) / "container" / "build.sh"
assert build_sh.exists(), f"build.sh not found at {build_sh}"
return str(build_sh)
def run_build_script(build_script_path, args, expect_failure=False):
"""
Run build.sh with specified arguments and return the result.
Args:
build_script_path: Path to build.sh
args: List of arguments to pass to build.sh
expect_failure: If True, expect non-zero exit code
Returns:
tuple: (exit_code, stdout, stderr)
"""
cmd = ["bash", build_script_path] + args
# Add placeholder environment variables required by build.sh
env = os.environ.copy()
env["commit_id"] = "commit_id"
env["current_tag"] = "current_tag"
env["latest_tag"] = "latest_tag"
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=env)
if not expect_failure and result.returncode != 0:
print(f"Command failed: {' '.join(cmd)}")
print(f"Exit code: {result.returncode}")
print(f"Stdout:\n{result.stdout}")
print(f"Stderr:\n{result.stderr}")
return result.returncode, result.stdout, result.stderr
class TestBuildShTRTLLMDownload:
"""Test download intention scenarios for TRTLLM"""
def test_default_behavior_downloads(self, build_script_path):
"""Test that default behavior (no TRTLLM flags) defaults to download"""
args = ["--framework", "TRTLLM", "--dry-run"]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: true" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: false" in stdout
assert (
"Inferring download because both TENSORRTLLM_PIP_WHEEL and TENSORRTLLM_INDEX_URL are not set"
in stdout
)
def test_download_with_pip_wheel_only(self, build_script_path):
"""Test download with --tensorrtllm-pip-wheel flag only"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel",
"tensorrt-llm==1.2.0",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: true" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: false" in stdout
assert (
"Installing tensorrt-llm==1.2.0 trtllm version from default pip index"
in stdout
)
def test_download_with_pip_wheel_and_index_url(self, build_script_path):
"""Test download with both --tensorrtllm-pip-wheel and --tensorrtllm-index-url"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel",
"tensorrt-llm==1.2.0",
"--tensorrtllm-index-url",
"https://custom.pypi.org/simple",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: true" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: false" in stdout
assert (
"Installing tensorrt-llm==1.2.0 trtllm version from index: https://custom.pypi.org/simple"
in stdout
)
def test_download_with_commit(self, build_script_path):
"""Test download with --tensorrtllm-pip-wheel and --tensorrtllm-commit"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel",
"tensorrt-llm==1.2.0",
"--tensorrtllm-commit",
"abc123def456",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: true" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: false" in stdout
class TestBuildShTRTLLMInstall:
"""Test install from pre-built wheel directory scenarios"""
def test_install_with_wheel_dir_and_commit(self, build_script_path, temp_wheel_dir):
"""Test install with --tensorrtllm-pip-wheel-dir and --tensorrtllm-commit"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel-dir",
temp_wheel_dir,
"--tensorrtllm-commit",
"abc123def456",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: false" in stdout
assert "Intent to Install TRTLLM: true" in stdout
assert "Intent to Build TRTLLM: false" in stdout
class TestBuildShTRTLLMBuild:
"""Test build from source scenarios"""
def test_build_with_git_url_and_commit(self, build_script_path):
"""Test build with --tensorrtllm-git-url and --tensorrtllm-commit"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-git-url",
"https://github.com/NVIDIA/TensorRT-LLM",
"--tensorrtllm-commit",
"abc123def456",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: false" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: true" in stdout
assert "TRTLLM pip wheel output directory is: /tmp/trtllm_wheel/"
def test_build_with_git_url_and_wheel_dir(self, build_script_path, temp_wheel_dir):
"""Test build with --tensorrtllm-git-url and --tensorrtllm-pip-wheel-dir"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-git-url",
"https://github.com/NVIDIA/TensorRT-LLM",
"--tensorrtllm-pip-wheel-dir",
temp_wheel_dir,
"--tensorrtllm-commit",
"abc123def456",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: false" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: true" in stdout
class TestBuildShTRTLLMInvalidCombinations:
"""Test invalid/conflicting flag combinations"""
def test_build_with_git_url_requires_commit(self, build_script_path):
"""Test build with --tensorrtllm-git-url flag requires commit"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-git-url",
"https://github.com/NVIDIA/TensorRT-LLM",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(
build_script_path, args, expect_failure=True
)
# Git URL alone requires commit to be specified
assert exit_code != 0, "Script should fail without commit"
combined_output = stdout + stderr
assert (
"[ERROR] TRTLLM framework was set as a target but the TRTLLM_COMMIT variable was not set"
in combined_output
)
def test_install_with_wheel_dir(self, build_script_path, temp_wheel_dir):
"""Test install with --tensorrtllm-pip-wheel-dir flag"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel-dir",
temp_wheel_dir,
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code != 0, f"Script failed with exit code {exit_code}"
combined_output = stdout + stderr
assert (
"[ERROR] TRTLLM framework was set as a target but the TRTLLM_COMMIT variable was not set."
in combined_output
)
def test_conflicting_all_three_intentions(self, build_script_path, temp_wheel_dir):
"""Test that all three intentions together causes an error"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel",
"tensorrt-llm==1.2.0",
"--tensorrtllm-index-url",
"https://custom.pypi.org/simple",
"--tensorrtllm-pip-wheel-dir",
temp_wheel_dir,
"--tensorrtllm-git-url",
"https://github.com/NVIDIA/TensorRT-LLM",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(
build_script_path, args, expect_failure=True
)
assert exit_code != 0, "Script should have failed with conflicting flags"
combined_output = stdout + stderr
assert (
"[ERROR] Could not figure out the trtllm installation intent"
in combined_output
)
def test_wheel_dir_with_git_url_builds(self, build_script_path, temp_wheel_dir):
"""Test that --tensorrtllm-git-url takes precedence over --tensorrtllm-pip-wheel-dir"""
# Note: Based on the code, git-url takes precedence and triggers build intention
# The wheel-dir is used as output directory for the build
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-pip-wheel-dir",
temp_wheel_dir,
"--tensorrtllm-git-url",
"https://github.com/NVIDIA/TensorRT-LLM",
"--tensorrtllm-commit",
"abc123def456",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
# According to the code logic (line 678), git-url sets build=true
# And wheel-dir only sets install=true if git-url is NOT set (line 670)
# So git-url takes precedence and this should succeed with build intention
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: false" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: true" in stdout
def test_index_url_without_pip_wheel_fails(self, build_script_path):
"""Test that --tensorrtllm-index-url alone without pip-wheel fails"""
# According to the code, index-url alone doesn't trigger download
# Only when both index-url AND pip-wheel are set (line 686)
# So this should fail with an error about unclear intention
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-index-url",
"https://custom.pypi.org/simple",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(
build_script_path, args, expect_failure=True
)
# Should fail because no clear intention can be determined
assert exit_code != 0, "Script should fail with unclear intention"
combined_output = stdout + stderr
assert (
"[ERROR] Could not figure out the trtllm installation intent"
in combined_output
)
class TestBuildShTRTLLMFlagValidation:
"""Test individual flag parsing and validation"""
def test_tensorrtllm_commit_flag_parsed(self, build_script_path):
"""Test that --tensorrtllm-commit flag is properly parsed"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-git-url",
"https://github.com/NVIDIA/TensorRT-LLM",
"--tensorrtllm-commit",
"test-commit-hash",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: false" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: true" in stdout
def test_tensorrtllm_git_url_flag_parsed(self, build_script_path):
"""Test that --tensorrtllm-git-url flag is properly parsed"""
args = [
"--framework",
"TRTLLM",
"--tensorrtllm-git-url",
"https://custom-git-url.example.com/TensorRT-LLM",
"--tensorrtllm-commit",
"test-commit",
"--dry-run",
]
exit_code, stdout, stderr = run_build_script(build_script_path, args)
assert exit_code == 0, f"Script failed with exit code {exit_code}"
assert "Intent to Download TRTLLM: false" in stdout
assert "Intent to Install TRTLLM: false" in stdout
assert "Intent to Build TRTLLM: true" in stdout
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment