deepstream-import-vision-model
Use this skill to bring a supported object-detection vision model from HuggingFace or NVIDIA NGC into an NVIDIA DeepStream pipeline with end-to-end automation: ONNX download, SafeTensors export, TRT engine build, custom nvinfer bbox parser, multi-stream benchmark, and PDF report.
Install
npx skills add https://github.com/NVIDIA/skills/tree/main/skills/deepstream-import-vision-model
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install nvidia-skills@llmmart
git clone https://github.com/NVIDIA/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole nvidia/skills collection as a plugin from our marketplace. Git is the plain clone.
README
DeepStream Import Vision Model
Automated end-to-end pipeline: HuggingFace model → TensorRT engine → DeepStream multi-stream benchmark → PDF report.
Overview
This self-contained skill uses four phase-specific reference documents. Together they automate the full model bringup workflow for NVIDIA DeepStream, from downloading a model on HuggingFace or NVIDIA NGC to a publication-ready benchmark report.
Supported input formats: ONNX (direct), SafeTensors (auto-exported via torch.onnx.export).
Current scope: object detection models only. Classification, segmentation, pose estimation, and other vision tasks are not yet supported — the pipeline fails fast if a non-detection architecture is detected in config.json.
Prerequisites
Host: only Docker + the NVIDIA driver. Everything else runs inside the DeepStream container —
DeepStream, TensorRT/trtexec, the Python export venv (torch/onnx/onnxruntime), wkhtmltopdf,
deepstream-app/gst-launch-1.0 — and is bootstrapped by setup.sh. Nothing is installed on the host.
Runs identically on Linux and Windows (Docker Desktop + WSL2 backend, required for GPU).
- Docker — Docker Desktop with the WSL2 backend on Windows; Docker Engine + NVIDIA Container Toolkit on Linux.
- NVIDIA GPU + driver (on Windows, the WSL2 GPU driver — no host CUDA/TensorRT/DeepStream needed).
docker pull nvcr.io/nvidia/deepstream:9.1-triton-multiarch, then runsetup.shthrough the container.
See references/windows.md for the cross-platform run model and the
per-shell docker run mount token.
Installation
bash <path-to-deepstream-import-vision-model>/install.sh --target <your-project-path>
Preview what will be installed first with --dry-run:
bash <path-to-deepstream-import-vision-model>/install.sh --target <your-project-path> --dry-run
Where <path-to-deepstream-import-vision-model> is the location of this skill in your repo, e.g.:
- In team-mind-hub:
team-skills/deepstream-sdk/deepstream-import-vision-model - In ds-copilot:
team-skills/deepstream-sdk/deepstream-import-vision-model(same path)
The script copies the complete skill into the target project for Claude Code, Codex, and Cursor. Re-running it safely refreshes an existing installation.
Usage
Claude Code:
Use deepstream-import-vision-model to run this model: https://huggingface.co/onnx-community/yolov8n
Codex:
Use $deepstream-import-vision-model to deploy and benchmark https://huggingface.co/onnx-community/yolov8n
Cursor:
@deepstream-import-vision-model run this model: https://huggingface.co/onnx-community/yolov8n
The skill runs the full pipeline autonomously — no manual steps required.
Pipeline Steps
| Step | Phase reference | Action |
|---|---|---|
| 1–3 | references/model-acquire.md |
Browse HF repo, download ONNX or export SafeTensors |
| 4–5 | references/engine-build.md |
Build dynamic TRT engine, run trtexec benchmarks |
| 6–7 | references/pipeline-run.md |
Custom bbox parser, DeepStream single + multi-stream |
| 8 | references/report-generation.md |
5 charts, HTML report, PDF |
Output Structure
Per-model outputs are written to models/<model_name>/ in your project:
models/<model_name>/
model/ ONNX file(s)
parser/ Custom nvinfer bbox parser (.cpp, .so)
config/ nvinfer config, DS app config, labels.txt
scripts/ Model-specific run helpers
benchmarks/ TRT engines, trtexec logs
reports/ benchmark_report.md / .html / .pdf + charts/
samples/ Output videos, test frames, KITTI detections
Files in this package
deepstream-import-vision-model/
├── SKILL.md Top-level skill definition
├── README.md This file
├── references/ Phase-specific runbooks
├── scripts/ Utility scripts by pipeline phase
└── tests/ Installer and script regression tests
Skill manifest
DeepStream Import Vision Model
When this skill is active, read the relevant reference document before starting each phase. Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules.
Current scope: Object detection models only. Fail fast on classification, segmentation, or other architectures detected in config.json.
Model choice — always offer two options
Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the user's request already clearly selects a model, confirm the matching choice instead of asking redundantly.
1. Default model (recommended)
Use the validated Hugging Face RT-DETR model:
model_id: PekingU/rtdetr_r50vd
source: huggingface
task: object-detection
precision_preference: fp16
2. Custom object-detection model
Ask for one supported source:
- Hugging Face model ID (
organization/model) or full model URL. - NVIDIA NGC catalog model URL including its version.
Explain that the skill currently rejects classification, segmentation, and other non-detection
architectures after inspecting config.json. Do not invent or silently substitute a model when the
custom source is missing or unsupported.
For a dry run, present the same two choices and simulate discovery, build, benchmark, and report stages without browsing, downloading, launching Docker, writing files, or starting processes.
Pipeline Overview
| Step | Phase | Reference | What it does |
|---|---|---|---|
| 1–3 | Model Acquire | references/model-acquire.md | Browse HF/NGC, detect format, download ONNX or export SafeTensors |
| 4–5 | Engine Build | references/engine-build.md | Build dynamic TRT engine, run trtexec BS=1 and BS=MAX_BS |
| 6–7 | DS Pipeline | references/pipeline-run.md | Custom bbox parser, nvinfer config, single-stream + multi-stream benchmarks |
| 8 | Report | references/report-generation.md | 5 charts, HTML, PDF benchmark report |
Run the full pipeline autonomously without pausing for confirmation at each step.
Runs entirely through Docker (no host packages)
Every step runs INSIDE the DeepStream container. The host needs only Docker + the NVIDIA
driver — no host python/venv/torch/trtexec/make/wkhtmltopdf. This works identically on Linux and
Windows (Docker Desktop + WSL2 backend, required for --gpus). The per-shell bind-mount
token is the only OS difference — -v "$PWD":/work (bash), -v "${PWD}:/work" (PowerShell),
-v "%cd%:/work" (cmd); full guide in references/windows.md. All venv/ONNX/
engine/parser/config/report artifacts live under the mounted working root and persist between the
ephemeral --rm containers.
Pre-flight — bootstrap + verify (through the container)
1. One-time bootstrap — builds build/.venv_optimum (torch/onnx/onnxruntime/report deps; the
venv name is historical, optimum is no longer used) +
installs wkhtmltopdf, all in-container. From the working root:
docker run --rm -it --gpus all --shm-size=16g -v "$PWD":/work -w /work \
--entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \
.claude/skills/deepstream-import-vision-model/setup.sh
2. Preflight — GPU + venv + trtexec, run THROUGH the container (container-mode auto-detects):
docker run --rm --gpus all -v "$PWD":/work -w /work \
--entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \
.claude/skills/deepstream-import-vision-model/scripts/preflight.sh # proceed only on PASS
Every subsequent phase runs the same way — issue the model's commands via
docker run … --entrypoint bash … -lc '<commands>' (or the
.claude/skills/deepstream-import-vision-model/scripts/dsrun.sh wrapper:
bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '<in-container command>'),
using PY=build/.venv_optimum/bin/python and
trtexec at /usr/src/tensorrt/bin/trtexec inside the container. deepstream-app,
gst-launch-1.0, and /opt/nvidia/deepstream/… sample paths all exist in the image.
TensorRT build+runtime share one image, so there is no version skew (the concern the old
"build on the host" rule tried to avoid — see references/engine-build.md).
sample_720p.mp4 ships in the image; set DS_VIDEO only to override.
Mandatory Output Structure
Create once MODEL_NAME is known (Step 1). Never dump files flat.
models/{model_name}/
model/ <- ONNX file(s)
parser/ <- .cpp, Makefile, .so
config/ <- nvinfer config, ds-app config, labels.txt
scripts/ <- run helper scripts
benchmarks/
engines/ <- _dynamic_b{MAX_BS}.engine, timing.cache, build logs
b1/ <- trtexec BS=1 log
b{MAX_BS}/ <- trtexec BS=MAX_BS log
ds/ <- DS benchmark logs
reports/ <- benchmark_report.md, .html, .pdf, benchmark_data.json
charts/ <- chart_*.png (5 charts)
samples/ <- output .mp4 or .ogv (theoraenc fallback), test frames
kitti_output/ <- KITTI detection .txt files
mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,benchmarks/ds,reports/charts,samples/kitti_output}
Critical Rules
- Engine naming — always
{model}_dynamic_b{MAX_BS}.engine. Never baremodel_dynamic.engine. - batch_size == num_streams — in DS runs,
batch-sizeand stream count are always equal. - Log filenames are fixed —
trtexec_b1.log,trtexec_b${MAX_BS}.log,ds_s${N}_run1.log,ds_s${N}_run2.log. No timestamps. Report generation reads exact paths. - Parser zero-init — always
NvDsInferObjectDetectionInfo obj = {};. Required for DS 9.1 OBB support; bareobj;leavesrotation_angleuninitialized, causing tilted bounding boxes. - KITTI validation gate — do NOT proceed to Step 7 if KITTI frame count is zero or detection rate < 90%.
- Shared venv —
build/.venv_optimumreused across all models. Never create per-model venvs. - trtexec
--noDataTransfers— GPU-only compute matches DeepStream's GPU-to-GPU data flow. - Report HTML+PDF — always use
.claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py. Never write a custom HTML generator or callwkhtmltopdfdirectly. - Object detection only — reject non-detection architectures from
config.jsonbefore building anything. - Encoder fallback (MANDATORY) —
x264encandopenh264encare prohibited. On NVENC-unavailable systems, usetheoraenc + oggmux(LGPL; ships in gst-plugins-base; output is.ogv). Iftheoraenc/oggmuxare absent, skip video creation (DS_SINGLE_STREAM_MODE=skipped). Report which mode was used:nvv4l2h264enc/theoraenc-fallback/skipped. - Video source (MANDATORY) — default is always
sample_720p.mp4(1280×720). Never autonomously substitutesample_1080p_h264.mp4or any other file. Only use a different video when the user explicitly provides a path (viaDS_VIDEOenv var or script argument).
Examples
Default model, end to end. Bootstrap once, then run the full pipeline:
docker run --rm -it --gpus all --shm-size=16g -v "$PWD":/work -w /work \
--entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \
.claude/skills/deepstream-import-vision-model/setup.sh
# then: "Use deepstream-import-vision-model to run PekingU/rtdetr_r50vd"
SafeTensors model with no published ONNX. Step 2b exports it first; the wrapper reports which backend produced the graph and fails loudly if the batch dimension was baked in:
bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \
models/$MODEL_NAME/hf_model models/$MODEL_NAME/onnx_export/
# [export] backend=dynamo
# [export] dynamo produced a static batch dimension; trying the next backend
# [export] backend=legacy-torchscript
# [export] pixel_values shape=['batch', 3, 640, 640]
Pin a Hub revision for a reproducible build — any exporter flag passes straight through:
bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \
PekingU/rtdetr_r50vd models/rtdetr/onnx_export --revision <commit-sha> --opset 18
Pipeline Timing
Wrap every step:
STEP_START=$(date +%s.%N)
# ... step commands ...
STEP_END=$(date +%s.%N)
STEP_DURATION=$(python3 -c "print(round($STEP_END - $STEP_START, 2))") # bc is not in the container; python3 always is
echo "[Step N] completed in ${STEP_DURATION}s"
Track PIPELINE_START (before Step 1) and PIPELINE_END (after Step 8). Report all durations in the benchmark report.
Report Output (MANDATORY — all 3 formats)
benchmark_report.md— markdown source (12 mandatory sections)benchmark_report.html— styled HTML (charts base64-inlined, no local file access)benchmark_report_{model_name}.pdf— viamd-to-html-pdf.py; verify charts are embedded by countingdata:image/pngoccurrences in the HTML output:grep -o 'data:image/png' benchmark_report.html | wc -lshould equal 5
Run charts and report scripts with the shared venv active: source build/.venv_optimum/bin/activate.
Reference Documents
IMPORTANT: Read the relevant reference before starting each phase. Do NOT generate code from memory.
| Document | Use When |
|---|---|
| references/model-acquire.md | Steps 1–3: HF/NGC URL parsing, format detection, ONNX download, SafeTensors export, label extraction |
| references/engine-build.md | Steps 4–5: trtexec engine build, benchmarks, PEAK_GPU_STREAMS derivation, iterative scaling |
| references/pipeline-run.md | Steps 6–7: custom bbox parser, nvinfer config, single-stream validation, KITTI dump, multi-stream benchmark |
| references/report-generation.md | Step 8: benchmark_data.json, 5 charts, 12-section markdown report, HTML + PDF |
Scripts
Installed into .claude/skills/deepstream-import-vision-model/scripts/ by install.sh.
| Script | Phase | Purpose |
|---|---|---|
model/hf-list-files.sh |
1–3 | List HuggingFace repo files |
model/hf-download-config.sh |
1–3 | Download config.json from HF |
model/ngc-list-files.sh |
1–3 | List NGC model files |
model/ngc-download.sh |
1–3 | Download NGC model archive |
model/safetensors-to-onnx.sh |
1–3 | Export SafeTensors → ONNX via torch.onnx.export (wrapper) |
model/safetensors_to_onnx.py |
1–3 | The exporter — dynamo backend, TorchScript fallback, verifies dynamic batch |
model/inspect-onnx.py |
1–5 | Inspect ONNX input/output shapes |
model/make-static-batch-onnx.py |
4–5 | Bake batch dim into ONNX |
model/cleanup.sh |
Any | Remove staging dirs, preserve shared venv |
engine/benchmark-trtexec.sh |
4–5 | Run trtexec with standard flags |
deepstream/ds-single-stream.sh |
6–7 | Single-stream visual validation (NVENC primary; theoraenc+oggmux fallback; skip if neither) |
deepstream/ds-sweep.sh |
6–7 | 2-phase batch size sweep |
deepstream/benchmark-ds.sh |
6–7 | Fixed-stream DS benchmark |
deepstream/ds-kitti-dump.sh |
6–7 | KITTI detection dump via deepstream-app |
deepstream/ds-perf-run.sh |
7 | Step 7c two-run benchmark — wraps deepstream-app with enable-perf-measurement=1, writes fixed-name log for the report parser |
deepstream/extract-frame.sh |
6–7 | Extract sample frames from output video (.mp4 NVENC path or .ogv theoraenc fallback) |
report/generate-benchmark-charts.py |
8 | Generate 5 benchmark PNG charts |
report/md-to-html-pdf.py |
8 | Markdown → styled HTML → PDF (canonical benchmark report path) |
report/md-to-pdf.sh |
Any | Markdown → PDF via pandoc/pdflatex — for design docs and references only, NOT for benchmark reports (use md-to-html-pdf.py for those) |
report/report-style.css |
8 | CSS for HTML report |
report/render-mermaid-for-pdf.py |
8 | Mermaid diagram → PNG |
report/mermaid-puppeteer.json |
8 | Vetted Puppeteer config for Mermaid (sandboxed; non-root) |
report/mermaid-puppeteer-root.json |
8 | Vetted Puppeteer config for Mermaid (used when running as root) |
Quick Error Reference
| Error | Fix |
|---|---|
| Tilted/diagonal bounding boxes | Parser struct not zero-initialized — use NvDsInferObjectDetectionInfo obj = {}; |
| Zero KITTI files | gie-kitti-output-dir not read by nvinfer — use ds-kitti-dump.sh (wraps deepstream-app) |
| Engine rebuilds every DS run | model-engine-file path wrong — check relative path from config/ dir |
setDimensions negative dims |
Add infer-dims=3;H;W to nvinfer config for dynamic ONNX models |
--memPoolSize workspace 0.03 MiB |
Use M suffix not MiB — e.g. --memPoolSize=workspace:32768M |
| ForeignNode build failure (DETR) | Run onnxsim — see references/engine-build.md. Not reproduced on TRT 10.16 with either export backend |
| ONNX has a static batch dim | Both export backends specialized it — see the gotchas in references/model-acquire.md |
| Zero detections | Wrong net-scale-factor — check model family table in references/pipeline-run.md |
No module named 'pyservicemaker' |
Install into venv: pip install /opt/nvidia/deepstream/.../pyservicemaker*.whl |
Files (skills)
-
agents
-
openai.yaml 384 B
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 interface: display_name: "DeepStream Import Vision Model" short_description: "Deploy object detectors into DeepStream" default_prompt: "Use $deepstream-import-vision-model to deploy and benchmark this object-detection model in DeepStream."
-
-
evals
-
evals.json 24.1 KB
{ "skill_name": "deepstream-import-vision-model", "evals": [ { "id": "1", "name": "hf-onnx-to-trt-deepstream-full-pipeline", "prompt": "Use deepstream-import-vision-model to run this model: https://huggingface.co/onnx-community/yolov8n", "expected_output": "Skill detects ONNX files in the HuggingFace repo, downloads the fp16 ONNX variant, builds a dynamic TensorRT engine with trtexec, runs DeepStream single-stream validation, enforces the KITTI detection-rate gate, executes multi-stream benchmarks, and generates the HTML/PDF report.", "files": [], "should_trigger": true, "expected_behavior": [ "Reads SKILL.md before acting", "Lists the HuggingFace repo files and selects the fp16 ONNX variant without prompting the user", "Builds a dynamic-batch TensorRT engine with trtexec", "Runs single-stream DeepStream validation and enforces the KITTI detection-rate gate before benchmarking", "Runs the multi-stream benchmark and generates the report in all three formats" ], "assertions": [ { "text": "Skill acknowledges the HuggingFace URL", "type": "contains_pattern", "pattern": "(huggingface|yolov8n|onnx-community)" }, { "text": "Skill selects the fp16 ONNX variant by filename", "type": "contains_pattern", "pattern": "(model_fp16\\.onnx|_fp16\\.onnx)" }, { "text": "Skill builds a dynamic-shape TensorRT engine with trtexec", "type": "contains_pattern", "pattern": "trtexec[\\s\\S]{0,400}(minShapes|optShapes|maxShapes|dynamic)" }, { "text": "Skill enforces the KITTI detection-rate validation gate before benchmarking", "type": "contains_pattern", "pattern": "KITTI[\\s\\S]{0,200}(90|detection rate)" }, { "text": "Skill runs the DeepStream pipeline", "type": "contains_pattern", "pattern": "(deepstream-app|gst-launch|nvinfer)" }, { "text": "Skill generates the benchmark report", "type": "contains_pattern", "pattern": "(md-to-html-pdf|\\.pdf|PDF|benchmark report)" }, { "text": "Skill does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "2", "name": "safetensors-torch-onnx-auto-export", "prompt": "Use deepstream-import-vision-model to run this model: https://huggingface.co/IDEA-Research/grounding-dino-base", "expected_output": "Skill finds only SafeTensors files in the HuggingFace repo, automatically exports to ONNX using torch.onnx.export via safetensors-to-onnx.sh in the shared build/.venv_optimum environment (dynamo backend with TorchScript fallback), then proceeds with TensorRT engine build and DeepStream integration.", "files": [], "should_trigger": true, "expected_behavior": [ "Reads SKILL.md and the model-acquire reference before acting", "Determines the repo has no ONNX files and only SafeTensors weights", "Exports to ONNX with torch.onnx.export inside the shared build/.venv_optimum environment", "Falls back from the dynamo backend to TorchScript if the export specializes the batch dimension", "Proceeds to the TensorRT engine build and DeepStream pipeline after a successful export" ], "assertions": [ { "text": "Skill detects no ONNX files and finds SafeTensors", "type": "contains_pattern", "pattern": "(SafeTensors|safetensors|no ONNX|only SafeTensors)" }, { "text": "Skill exports to ONNX via torch.onnx.export", "type": "contains_pattern", "pattern": "(torch\\.onnx\\.export|safetensors[-_]to[-_]onnx)" }, { "text": "Skill uses the shared build/.venv_optimum export environment", "type": "contains_pattern", "pattern": "build/\\.venv_optimum" }, { "text": "Skill preserves a dynamic batch dimension in the exported ONNX", "type": "contains_pattern", "pattern": "(dynamic[\\s_-]?(batch|axes)|dynamo|TorchScript)" }, { "text": "Skill proceeds to TensorRT after export", "type": "contains_pattern", "pattern": "(trtexec|TensorRT engine)" }, { "text": "Skill runs DeepStream pipeline after engine build", "type": "contains_pattern", "pattern": "(deepstream-app|gst-launch|nvinfer)" }, { "text": "Skill does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "3", "name": "bring-your-own-vision-model-routing", "prompt": "Bring my own vision model and benchmark this model on DeepStream: https://huggingface.co/onnx-community/yolov8n", "expected_output": "Skill recognises the bring-your-own-model request and runs the full deepstream-import-vision-model pipeline — same behaviour as invoking deepstream-import-vision-model directly.", "files": [], "should_trigger": true, "expected_behavior": [ "Discovers and activates deepstream-import-vision-model without the user naming it", "Proceeds directly into the import pipeline instead of asking which skill or agent to use", "Runs the ONNX download, TensorRT engine build, DeepStream pipeline, and benchmark report" ], "assertions": [ { "text": "Skill activates and acknowledges the canonical workflow", "type": "contains_pattern", "pattern": "(deepstream-import-vision-model|bring.*vision.*model|BYOVM)" }, { "text": "Skill processes the HuggingFace URL", "type": "contains_pattern", "pattern": "(huggingface|yolov8n)" }, { "text": "Skill runs the full pipeline", "type": "contains_pattern", "pattern": "(trtexec|TensorRT|DeepStream|benchmark)" }, { "text": "Skill does not ask the user which agent or skill to use", "type": "not_contains_pattern", "pattern": "\\b(which|what)\\s+(agent|skill)\\b[^.?\\n]{0,80}\\?" } ] }, { "id": "4", "name": "ngc-tao-model-builtin-parser", "prompt": "Use deepstream-import-vision-model to run the trafficcamnet model from NVIDIA NGC", "expected_output": "Skill downloads the NGC TAO model using the NGC CLI or authenticated HTTPS fallback, uses the built-in DeepStream TAO parser (libnvds_infercustomparser.so with parse-bbox-func-name) instead of compiling a custom parser, reads preprocessing params from nvdsinfer_config.yaml, and runs the full benchmark pipeline.", "files": [], "should_trigger": true, "expected_behavior": [ "Reads SKILL.md and the pipeline-run reference before acting", "Downloads the versioned NGC model with the NGC CLI or the authenticated HTTPS fallback", "Routes to the built-in TAO parser library rather than generating and compiling parser source", "Reads preprocessing parameters from the model's nvdsinfer config instead of guessing them", "Runs the DeepStream benchmark pipeline and generates the report" ], "assertions": [ { "text": "Skill downloads from NGC using a supported client", "type": "contains_pattern", "pattern": "(ngc|NGC|trafficcamnet|api\\.ngc\\.nvidia\\.com)" }, { "text": "Skill names the built-in TAO parser library", "type": "contains_pattern", "pattern": "libnvds_infercustomparser\\.so" }, { "text": "Skill wires the parser via parse-bbox-func-name", "type": "contains_pattern", "pattern": "parse-bbox-func-name" }, { "text": "Skill does not invoke a compiler to build a custom parser", "type": "not_contains_pattern", "pattern": "(^|\\n)\\s*(sudo\\s+)?(g\\+\\+|gcc|cmake|make)\\s+[^\\n]*\\.(cpp|c|o)\\b" }, { "text": "Skill runs DeepStream pipeline", "type": "contains_pattern", "pattern": "(deepstream-app|gst-launch|nvinfer)" }, { "text": "Skill does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "5", "name": "custom-local-onnx-model", "prompt": "Use deepstream-import-vision-model to run my local ONNX model at /workspace/models/my_detector.onnx", "expected_output": "Skill skips the HuggingFace download steps entirely, inspects the local ONNX file directly with inspect-onnx.py, builds a TensorRT engine, and proceeds with DeepStream integration and benchmarking.", "files": [], "should_trigger": true, "expected_behavior": [ "Recognises the input as an on-disk ONNX path and skips every download step", "Inspects the local ONNX inputs, outputs, and opset before building", "Builds the TensorRT engine directly from the local file", "Runs the DeepStream pipeline and benchmark against the resulting engine" ], "assertions": [ { "text": "Skill acknowledges the local ONNX path", "type": "contains_pattern", "pattern": "(my_detector|/workspace/models|local.*ONNX|ONNX.*local)" }, { "text": "Skill does not execute a HuggingFace download", "type": "not_contains_pattern", "pattern": "(^|\\n)\\s*(hf_hub_download\\(|huggingface-cli\\s+download|hf\\s+download\\b)" }, { "text": "Skill inspects the ONNX model", "type": "contains_pattern", "pattern": "(inspect-onnx|inspect|input.*shape|output.*shape|opset)" }, { "text": "Skill builds TensorRT engine from local model", "type": "contains_pattern", "pattern": "(trtexec|TensorRT engine)" }, { "text": "Skill runs DeepStream pipeline", "type": "contains_pattern", "pattern": "(deepstream-app|gst-launch|nvinfer)" } ] }, { "id": "6", "name": "multi-variant-onnx-fp16-auto-select", "prompt": "Use deepstream-import-vision-model to run this model: https://huggingface.co/onnx-community/owlvit-base-patch32", "expected_output": "Skill finds multiple ONNX variants (fp16, fp32, quantized) in the HuggingFace repo, automatically selects the fp16 variant without prompting the user, and applies OWL-ViT's exact preprocessing constants (net-scale-factor=0.01459, cluster-mode=2) in the nvinfer config.", "files": [], "should_trigger": true, "expected_behavior": [ "Lists the repo and identifies more than one ONNX variant", "Applies the deterministic fp16-first selection rule silently, without asking the user to choose", "Supplies OWL-ViT's exact nvinfer preprocessing constants rather than generic defaults", "Proceeds to the TensorRT engine build with the selected variant" ], "assertions": [ { "text": "Skill detects multiple ONNX variants", "type": "contains_pattern", "pattern": "(multiple|variant|fp32|quantized)" }, { "text": "Skill selects the fp16 variant by filename", "type": "contains_pattern", "pattern": "(model_fp16\\.onnx|_fp16\\.onnx)" }, { "text": "Skill supplies the OWL-ViT net-scale-factor constant", "type": "contains_pattern", "pattern": "net-scale-factor\\s*=\\s*0\\.01459" }, { "text": "Skill supplies the OWL-ViT clustering mode", "type": "contains_pattern", "pattern": "cluster-mode\\s*=\\s*2" }, { "text": "Skill does not ask the user which variant or precision to use", "type": "not_contains_pattern", "pattern": "\\b(which|what)\\s+(ONNX\\s+)?(variant|precision|model file)\\b[^.?\\n]{0,80}\\?" }, { "text": "Skill proceeds with TensorRT engine build", "type": "contains_pattern", "pattern": "(trtexec|TensorRT engine)" } ] }, { "id": "7", "name": "reject-unsupported-non-vision-model", "prompt": "Use deepstream-import-vision-model to run this model: https://huggingface.co/openai-community/gpt2", "expected_output": "Skill identifies that gpt2 is a language model, not an object-detection vision model, cites the config.json architecture gate (ForCausalLM), and rejects the request. Skill does not download weights, build a TensorRT engine, or start a DeepStream pipeline.", "files": [], "should_trigger": false, "expected_behavior": [ "Inspects or reasons about the model's config.json architecture before doing any work", "Identifies the architecture as a causal language model and therefore out of scope", "Explains that the skill supports object-detection vision models only", "Does not execute any skill script, download weights, build an engine, or start a pipeline" ], "assertions": [ { "text": "Skill identifies the model is not an object-detection vision model", "type": "contains_pattern", "pattern": "(language model|NLP|not a vision|unsupported|not compatible|text model|object detection only)" }, { "text": "Skill cites the config.json architecture gate", "type": "contains_pattern", "pattern": "(ForCausalLM|architecture|config\\.json)" }, { "text": "Skill does not execute an engine build or DeepStream run", "type": "not_contains_pattern", "pattern": "(^|\\n)\\s*(bash\\s+)?(\\./)?((scripts/)?(engine/benchmark-trtexec\\.sh|deepstream/[a-z-]+\\.sh)|trtexec\\s+--|deepstream-app\\s+-c|gst-launch-1\\.0\\s)" }, { "text": "Skill does not leak credentials", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "disco-hf-onnx-no-skill-name", "name": "disco-hf-onnx-no-skill-name", "prompt": "I have a YOLOv8 ONNX detector on HuggingFace — get it running on DeepStream and give me a throughput benchmark.", "expected_output": "Without being told which skill to use, the agent discovers deepstream-import-vision-model from the intent, then runs the full ONNX download → TensorRT engine → DeepStream pipeline → benchmark report path.", "files": [], "should_trigger": true, "expected_behavior": [ "Discovers deepstream-import-vision-model from the intent without the user naming it", "Reads SKILL.md before acting", "Runs the ONNX acquisition, TensorRT engine build, and DeepStream benchmark end to end", "Produces the benchmark report" ], "assertions": [ { "text": "Agent routes to the DeepStream vision-model import workflow", "type": "contains_pattern", "pattern": "(deepstream-import-vision-model|import.*vision.*model)" }, { "text": "Agent builds a TensorRT engine with trtexec", "type": "contains_pattern", "pattern": "(trtexec|TensorRT engine)" }, { "text": "Agent runs the DeepStream pipeline and benchmark", "type": "contains_pattern", "pattern": "(deepstream-app|gst-launch|nvinfer)" }, { "text": "Agent does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "disco-ngc-trafficcamnet-no-skill-name", "name": "disco-ngc-trafficcamnet-no-skill-name", "prompt": "Benchmark the NGC TrafficCamNet model in a DeepStream multi-stream pipeline.", "expected_output": "Without being told which skill to use, the agent discovers deepstream-import-vision-model, downloads the versioned NGC TAO model, routes to the built-in TAO parser (libnvds_infercustomparser.so), and runs the multi-stream benchmark.", "files": [], "should_trigger": true, "expected_behavior": [ "Discovers deepstream-import-vision-model from the intent without the user naming it", "Downloads the versioned NGC TAO model", "Routes to the built-in TAO parser rather than compiling parser source", "Runs the multi-stream DeepStream benchmark and reports throughput" ], "assertions": [ { "text": "Agent routes to the DeepStream vision-model import workflow", "type": "contains_pattern", "pattern": "(deepstream-import-vision-model|import.*vision.*model)" }, { "text": "Agent names the built-in TAO parser library", "type": "contains_pattern", "pattern": "libnvds_infercustomparser\\.so" }, { "text": "Agent runs a multi-stream DeepStream benchmark", "type": "contains_pattern", "pattern": "(multi-?stream|num-sources|batch[_-]?size)" }, { "text": "Agent does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "disco-safetensors-export-no-skill-name", "name": "disco-safetensors-export-no-skill-name", "prompt": "Export this SafeTensors detection model to ONNX and benchmark it on DeepStream: https://huggingface.co/IDEA-Research/grounding-dino-base", "expected_output": "Without being told which skill to use, the agent discovers deepstream-import-vision-model and commits to the shared build/.venv_optimum torch.onnx.export path rather than hunting for a community ONNX conversion.", "files": [], "should_trigger": true, "expected_behavior": [ "Discovers deepstream-import-vision-model from the intent without the user naming it", "Commits to exporting the weights itself with torch.onnx.export in the shared environment", "Does not defer the export by suggesting the user find a pre-converted community ONNX", "Proceeds to the TensorRT engine build and DeepStream benchmark" ], "assertions": [ { "text": "Agent commits to torch.onnx.export", "type": "contains_pattern", "pattern": "(torch\\.onnx\\.export|safetensors[-_]to[-_]onnx)" }, { "text": "Agent uses the shared build/.venv_optimum export environment", "type": "contains_pattern", "pattern": "build/\\.venv_optimum" }, { "text": "Agent proceeds to the TensorRT engine build", "type": "contains_pattern", "pattern": "(trtexec|TensorRT engine)" }, { "text": "Agent does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "reject-classification-resnet50", "name": "reject-classification-resnet50", "prompt": "Import a ResNet-50 image classifier into DeepStream.", "expected_output": "Skill identifies image classification as out of scope (object detection only, Critical Rule #9) and declines before downloading weights or building an engine. Tests the architecture boundary rather than the modality boundary.", "files": [], "should_trigger": false, "expected_behavior": [ "Identifies the model as an image classifier, not an object detector", "States that the skill supports object-detection models only", "Does not execute any skill script, download weights, build an engine, or start a pipeline" ], "assertions": [ { "text": "Agent identifies classification as out of scope", "type": "contains_pattern", "pattern": "(classification|classifier|not.*object detection|object detection only|out of scope|unsupported)" }, { "text": "Agent does not execute an engine build or DeepStream run", "type": "not_contains_pattern", "pattern": "(^|\\n)\\s*(bash\\s+)?(\\./)?((scripts/)?(engine/benchmark-trtexec\\.sh|deepstream/[a-z-]+\\.sh)|trtexec\\s+--|deepstream-app\\s+-c|gst-launch-1\\.0\\s)" }, { "text": "Agent does not leak credentials or secrets", "type": "not_contains_pattern", "pattern": "((Bearer|Authorization:)\\s+\\S{12,}|sk-[A-Za-z0-9]{16,}|(hf_|nvapi-|ghp_|glpat-)[A-Za-z0-9_-]{16,}|(api[_.-]?key|password|secret|token)\\s*[:=]\\s*['\"]?[A-Za-z0-9_+/=-]{12,})" } ] }, { "id": "reject-segmentation-unet", "name": "reject-segmentation-unet", "prompt": "Set up a DeepStream segmentation pipeline for my UNet model.", "expected_output": "Skill identifies semantic segmentation as out of scope — vision, but not object detection — and declines the import workflow. Tests the closest out-of-scope neighbour.", "files": [], "should_trigger": false, "expected_behavior": [ "Identifies the model as a segmentation network, not an object detector", "States that the skill supports object-detection models only", "Does not execute any skill script, build an engine, or start a pipeline" ], "assertions": [ { "text": "Agent identifies segmentation as out of scope", "type": "contains_pattern", "pattern": "(segmentation|not.*object detection|object detection only|out of scope|unsupported)" }, { "text": "Agent does not execute an engine build or DeepStream run", "type": "not_contains_pattern", "pattern": "(^|\\n)\\s*(bash\\s+)?(\\./)?((scripts/)?(engine/benchmark-trtexec\\.sh|deepstream/[a-z-]+\\.sh)|trtexec\\s+--|deepstream-app\\s+-c|gst-launch-1\\.0\\s)" } ] }, { "id": "non-activation-general-install-question", "name": "non-activation-general-install-question", "prompt": "How do I install the DeepStream SDK on Ubuntu?", "expected_output": "A general setup question. The agent answers conversationally and does not activate the model-import pipeline, download a model, or build an engine.", "files": [], "should_trigger": false, "expected_behavior": [ "Answers the installation question conversationally", "Does not activate the model-import workflow", "Does not download a model, build an engine, or run a benchmark" ], "assertions": [ { "text": "Agent answers the installation question", "type": "contains_pattern", "pattern": "(install|apt|deb|tarball|SDK)" }, { "text": "Agent does not run the import pipeline", "type": "not_contains_pattern", "pattern": "(^|\\n)\\s*(bash\\s+)?(\\./)?((scripts/)?(model/[a-z-]+\\.sh|engine/benchmark-trtexec\\.sh|deepstream/[a-z-]+\\.sh)|trtexec\\s+--)" } ] } ] }
-
-
references
-
engine-build.md 16.6 KB
# NV Engine Build -- Steps 4-5 Build a TensorRT engine from ONNX and derive PEAK_GPU_STREAMS for DeepStream sizing. The ONNX model path is: `$ARGUMENTS` ## Pre-flight: Validate Inputs and Extract Variables Before anything else, derive all variables from `$ARGUMENTS` and verify the environment: ```bash ONNX_PATH="$ARGUMENTS" # Derive MODEL_NAME from directory structure: models/{MODEL_NAME}/model/... MODEL_NAME=$(echo "$ONNX_PATH" | sed 's|models/\([^/]*\)/.*|\1|') # Derive MODEL_FILENAME as the ONNX basename without extension MODEL_FILENAME=$(basename "$ONNX_PATH" .onnx) # MAX_BS drives --optShapes, --maxShapes, and the engine filename postfix # Starting value is 64 — will double iteratively in Step 5 if PEAK_GPU_STREAMS > 64 MAX_BS=64 echo "Model: $MODEL_NAME" echo "File: $MODEL_FILENAME" echo "ONNX: $ONNX_PATH" echo "Engine: models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine" # Verify ONNX file exists ls -lh "$ONNX_PATH" || { echo "ERROR: ONNX file not found at $ONNX_PATH"; exit 1; } # trtexec lives at a fixed path IN the container (not on PATH); read the TRT version from it. TRTEXEC=/usr/src/tensorrt/bin/trtexec [ -x "$TRTEXEC" ] || { echo "ERROR: trtexec not found at $TRTEXEC — is this the DeepStream/TensorRT image?"; exit 1; } # `head -2` closes the pipe early, so trtexec takes SIGPIPE; under `set -o pipefail` that # surfaces as exit 141 and aborts the calling script. Engine/log existence is checked # explicitly below, so the version banner is advisory only. "$TRTEXEC" --version 2>&1 | head -2 || true # Verify GPU is available (in-container) nvidia-smi --query-gpu=name,memory.total --format=csv,noheader ``` If the ONNX file doesn't exist, inform the user to run Steps 1-3 first (see references/model-acquire.md). > All subsequent commands use `$MODEL_NAME`, `$MODEL_FILENAME`, `$MAX_BS`, and `$TRTEXEC` — never hardcoded paths or template placeholders. Inspect the ONNX model and auto-parse input name and spatial dimensions: ```bash INSPECT_OUT=$(python3 .claude/skills/deepstream-import-vision-model/scripts/model/inspect-onnx.py "$ONNX_PATH") echo "$INSPECT_OUT" INPUT_NAME=$(echo "$INSPECT_OUT" | grep -oP 'input_name:\s*\K\S+') H=$(echo "$INSPECT_OUT" | grep -oP 'height:\s*\K[0-9]+') W=$(echo "$INSPECT_OUT" | grep -oP 'width:\s*\K[0-9]+') echo "INPUT_NAME=$INPUT_NAME H=$H W=$W" [ -z "$INPUT_NAME" ] && { echo "ERROR: could not parse INPUT_NAME from inspect output"; exit 1; } # If H/W are empty (dynamic spatial dims), set them manually before proceeding: # H=640; W=640 # or whatever the model's expected input resolution is # Check the model card on HuggingFace or config.json image_size field [ -z "$H" ] && { echo "ERROR: H not detected — model has dynamic spatial dims. Set H manually: H=<height>"; exit 1; } [ -z "$W" ] && { echo "ERROR: W not detected — model has dynamic spatial dims. Set W manually: W=<width>"; exit 1; } ``` ## Step 4: Build TensorRT Engine Build one dynamic engine optimized for BS=64. `opt=max=64` ensures TRT optimizes kernels for the exact batch size used for benchmarking and DeepStream. `min=1` handles single-stream validation. ```bash STEP4_START=$(date +%s.%N) TIMESTAMP=$(date +%Y%m%d_%H%M%S) # benchmarks/engines/ already exists from model-acquire; # mkdir -p kept here as a safety net for standalone use mkdir -p models/$MODEL_NAME/benchmarks/engines models/$MODEL_NAME/benchmarks/b1 models/$MODEL_NAME/benchmarks/b${MAX_BS} $TRTEXEC \ --onnx="$ONNX_PATH" \ --minShapes=$INPUT_NAME:1x3x${H}x${W} \ --optShapes=$INPUT_NAME:${MAX_BS}x3x${H}x${W} \ --maxShapes=$INPUT_NAME:${MAX_BS}x3x${H}x${W} \ --fp16 \ --skipInference \ --memPoolSize=workspace:32768M \ --timingCacheFile=models/$MODEL_NAME/benchmarks/engines/timing.cache \ --saveEngine="models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine" \ 2>&1 | tee models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_build_${TIMESTAMP}.log # Verify engine was created — trtexec exit code is lost through the pipe, so check the file [ -f "models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine" ] || \ { echo "ERROR: Engine file not created — check build log for errors"; exit 1; } STEP4_END=$(date +%s.%N) STEP4_DURATION=$(python3 -c "print(round($STEP4_END - $STEP4_START, 2))") # bc is not in the container; python3 always is echo "[Step 4] Engine build completed in ${STEP4_DURATION}s" ``` Set the ENGINE variable — used by all subsequent trtexec and DeepStream runs: ```bash ENGINE="models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine" ``` ## Step 5: Benchmark — 2 Runs Only Run exactly **2 trtexec benchmarks** using the Step 4 engine. No sweep needed. - BS=1 → latency baseline (single-stream worst case) - BS=64 → peak throughput → `PEAK_GPU_STREAMS` ```bash STEP5_START=$(date +%s.%N) ``` ### Run 5a — Latency baseline (BS=1) > Log filename is **fixed** — no timestamp, no variation. Always `trtexec_b1.log`. This ensures the report-generation skill can find it with an exact path, not a wildcard. ```bash $TRTEXEC \ --loadEngine="$ENGINE" \ --shapes=$INPUT_NAME:1x3x${H}x${W} \ --noDataTransfers --duration=10 --warmUp=1000 \ 2>&1 | tee models/$MODEL_NAME/benchmarks/b1/trtexec_b1.log ``` ### Run 5b — Peak throughput (BS=MAX_BS) > Log filename is **fixed** — always `trtexec_b${MAX_BS}.log`. Updated by the while loop if MAX_BS changes. ```bash $TRTEXEC \ --loadEngine="$ENGINE" \ --shapes=$INPUT_NAME:${MAX_BS}x3x${H}x${W} \ --noDataTransfers --duration=10 --warmUp=1000 \ 2>&1 | tee models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log ``` ### Parse results and compute PEAK_GPU_STREAMS ```bash QPS_BS1=$(grep -oP 'Throughput:\s*\K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b1/trtexec_b1.log | tail -1) GPU_MEAN_BS1=$(grep -oP 'GPU Compute Time:.*mean = \K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b1/trtexec_b1.log | tail -1) QPS_BS_MAX=$(grep -oP 'Throughput:\s*\K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log | tail -1) GPU_MEAN_BS_MAX=$(grep -oP 'GPU Compute Time:.*mean = \K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log | tail -1) GPU_P99_BS_MAX=$(grep -oP 'GPU Compute Time:.*percentile\(99%\) = \K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log | tail -1) read IMGS_PER_SEC PEAK_GPU_STREAMS < <(python3 -c " import math imgs = float('$QPS_BS_MAX') * $MAX_BS streams = int(math.floor(imgs / 30)) print(round(imgs, 2), streams) ") echo "BS=1: QPS=$QPS_BS1 GPU mean=${GPU_MEAN_BS1}ms" echo "BS=$MAX_BS: QPS=$QPS_BS_MAX imgs/s=$IMGS_PER_SEC GPU mean=${GPU_MEAN_BS_MAX}ms P99=${GPU_P99_BS_MAX}ms" echo "PEAK_GPU_STREAMS=$PEAK_GPU_STREAMS (floor($IMGS_PER_SEC / 30))" STEP5_END=$(date +%s.%N) STEP5_DURATION=$(python3 -c "print(round($STEP5_END - $STEP5_START, 2))") # bc is not in the container; python3 always is echo "[Step 5] Benchmarks completed in ${STEP5_DURATION}s" ``` `PEAK_GPU_STREAMS` is the GPU-only upper bound on real-time 30fps stream count. DeepStream will always achieve fewer streams due to NVDEC, mux, and GStreamer overhead (typically 10–40%). Use `PEAK_GPU_STREAMS` as the starting stream count for DS Run 1 (calibration). ### Iterative Engine Scaling (PEAK_GPU_STREAMS > MAX_BS) If `PEAK_GPU_STREAMS > MAX_BS`, the engine's max batch size is the bottleneck — DeepStream cannot run more streams than `MAX_BS`. **Double MAX_BS and rebuild**, then re-run trtexec and recompute `PEAK_GPU_STREAMS`. Repeat until `PEAK_GPU_STREAMS ≤ MAX_BS`. **Why doubling, not jumping to PEAK directly**: Jumping from 64→512 based on an extrapolated projection wastes GPU memory if the projection was off. Doubling (64→128→256→512) makes incremental, verifiable steps — each trtexec run gives real throughput data before committing to a larger rebuild. ```bash while [ "$PEAK_GPU_STREAMS" -gt "$MAX_BS" ]; do NEW_MAX_BS=$(python3 -c "print($MAX_BS * 2)") # STRICT DOUBLING — do not change to ceil(log2(PEAK)) echo "Rebuilding engine: PEAK_GPU_STREAMS=$PEAK_GPU_STREAMS > MAX_BS=$MAX_BS — doubling to: $NEW_MAX_BS" mkdir -p models/$MODEL_NAME/benchmarks/b${NEW_MAX_BS} $TRTEXEC \ --onnx="$ONNX_PATH" \ --minShapes=$INPUT_NAME:1x3x${H}x${W} \ --optShapes=$INPUT_NAME:${NEW_MAX_BS}x3x${H}x${W} \ --maxShapes=$INPUT_NAME:${NEW_MAX_BS}x3x${H}x${W} \ --fp16 --skipInference \ --memPoolSize=workspace:32768M \ --timingCacheFile=models/$MODEL_NAME/benchmarks/engines/timing.cache \ --saveEngine="models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${NEW_MAX_BS}.engine" \ 2>&1 | tee models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_build_b${NEW_MAX_BS}_${TIMESTAMP}.log [ -f "models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${NEW_MAX_BS}.engine" ] || \ { echo "ERROR: Engine b${NEW_MAX_BS} not created — check build log"; exit 1; } # Update ENGINE and MAX_BS — re-run trtexec at new BS and recompute PEAK_GPU_STREAMS ENGINE="models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${NEW_MAX_BS}.engine" MAX_BS=$NEW_MAX_BS $TRTEXEC \ --loadEngine="$ENGINE" \ --shapes=$INPUT_NAME:${MAX_BS}x3x${H}x${W} \ --noDataTransfers --duration=10 --warmUp=1000 \ 2>&1 | tee models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log QPS_BS_MAX=$(grep -oP 'Throughput:\s*\K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log | tail -1) GPU_MEAN_BS_MAX=$(grep -oP 'GPU Compute Time:.*mean = \K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log | tail -1) GPU_P99_BS_MAX=$(grep -oP 'GPU Compute Time:.*percentile\(99%\) = \K[0-9.]+' \ models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log | tail -1) read IMGS_PER_SEC PEAK_GPU_STREAMS < <(python3 -c " import math imgs = float('$QPS_BS_MAX') * $MAX_BS print(round(imgs, 2), int(math.floor(imgs / 30))) ") echo "Recomputed: BS=$MAX_BS imgs/s=$IMGS_PER_SEC PEAK_GPU_STREAMS=$PEAK_GPU_STREAMS" done echo "PEAK_GPU_STREAMS ($PEAK_GPU_STREAMS) <= MAX_BS ($MAX_BS) — engine scaling complete." ``` **Engine count summary:** | Scenario | Example | Engines | trtexec runs | |----------|---------|---------|-------------| | PEAK_GPU_STREAMS ≤ 64 (transformer/large models) | RT-DETR, OWL-ViT | **1** (`b64`) | **2** | | PEAK_GPU_STREAMS > 64, ≤ 128 (mid models) | TrafficCamNet | **2** (`b64` + `b128`) | **3** | | PEAK_GPU_STREAMS > 128, ≤ 256 (fast models) | YOLO26n | **3** (`b64`+`b128`+`b256`) | **4** | | PEAK_GPU_STREAMS > 256 (very fast nano models) | — | **4+** (keep doubling) | **5+** | ## trtexec Flags Reference ### Recommended Flags | Flag | Purpose | When to use | |------|---------|-------------| | `--duration=10` | Longer run for stable numbers | All benchmark runs (5a, 5b) | | `--warmUp=1000` | 1s warmup before measurement | All benchmark runs (5a, 5b) | | `--noDataTransfers` | GPU-only compute (matches DS reality) | Always | ### Why GPU-only (`--noDataTransfers`) Only In DeepStream, frames are decoded on GPU (`nvv4l2decoder`) and stay on GPU through `nvinfer` — no H2D transfer. Standard trtexec transfers synthetic data from host, which is not representative. Do NOT report H2D/D2H latency. ### Flags That Do NOT Help (tested) | Flag | Result | Why | |------|--------|-----| | `--best` | No improvement | Engine already built with --fp16, runtime flag doesn't change precision | | `--exposeDMA` | **45% WORSE** throughput | Serializes DMA transfers — kills pipelining | | `--infStreams=4` | +2% QPS max | GPU already saturated | ### Key Metrics to Report from trtexec - **Throughput (QPS)** and **Images/s** (QPS × batch_size) - **GPU Compute mean (ms)** and **GPU Compute P99 (ms)** - **GPU Compute per image (ms)** (GPU Compute mean / batch_size) - Do **NOT** report: H2D latency, D2H latency, Host Latency, transfer overhead ## Engine Version Compatibility -- CRITICAL TensorRT engine files are **not portable** across TensorRT versions. ### Pre-flight Version Check `trtexec` lives in the container at `/usr/src/tensorrt/bin/trtexec` (set `TRTEXEC=/usr/src/tensorrt/bin/trtexec`); get its version with `"$TRTEXEC" --version`. No host `dpkg`/`libnvinfer-bin` check is needed. ### Build and run in the SAME container image The engine is **built and run inside the same image** (`nvcr.io/nvidia/deepstream:9.1-triton-multiarch`), so the `libnvinfer` that `trtexec` builds against is byte-for-byte the one DeepStream loads at runtime — **there is no build-vs-runtime version skew.** This is the correct fix for TensorRT's engine non-portability: it *eliminates* the "Docker-built engine silently fails on a differently-versioned host runtime" failure mode (0% GPU / stuck pipeline) that the old "always build on the host" rule tried to avoid. **Never build an engine against a different TensorRT than the one that will run it** — using one image for both guarantees this by construction. (Do not build engines on the host; the host has no required toolchain in the container-run model.) ## Known Issues and Workarounds ### `--memPoolSize` Flag Format — `M` vs `MiB` (CRITICAL silent failure) - **Correct**: `--memPoolSize=workspace:32768M` (suffix `M` = Mebibytes) - **WRONG**: `--memPoolSize=workspace:32768MiB` — trtexec interprets `MiB` as bytes, so `32768MiB` becomes 32 KB. All tactics fail with "insufficient workspace". There is no parse warning; the only symptom is `Memory Pools: workspace: 0.03125 MiB` in the build log. - Valid suffixes: `B`, `K`, `M`, `G`, or no suffix (default MiB). ### Deformable Attention Models (RT-DETR, DDETR, Deformable DETR) Models using `MultiscaleDeformableAttnPlugin_TRT` build correctly on TRT 10.16 **provided workspace is sufficient**. - **Required**: `--memPoolSize=workspace:32768M` (not the default 8GB) — deformable attention at BS=64 needs substantial workspace for ForeignNode fusion tactics. - `--builderOptimizationLevel=4` (default) works; do not lower it unless necessary. - Typical footprint at BS=64 on H100: activation ~4266 MiB, peak memory ~7809 MiB, build time ~825s. The compiler backend phase after engine generation can take 5-10 minutes with no log output — this is normal, not a hang. - Error "Could not find any implementation for node {ForeignNode[...]} due to insufficient workspace" is a genuine signal to raise the workspace. ### DETR / DETR-family Backbone Mask ForeignNode Failure (TRT 10.16) HF-exported DETR/DDETR models contain a dynamic backbone mask path (`Cast → Resize → Sigmoid`) that TRT 10.16 fuses into a ForeignNode with no valid tactic: `"Could not find any implementation for node {ForeignNode[.../Cast_2.../Sigmoid]}"`. - **Preferred fix** (TRT 10.16.01+, PyTorch 2.11+, transformers 5.5+): use the dynamo export path with `torch.export.Dim("batch", min=1, max=N)` in `dynamic_shapes`. The dynamo exporter produces a different graph that does NOT trigger the ForeignNode failure. TRT converts it directly as a dynamic-batch engine. - **Fallback for older toolchains**: run `onnxsim.simplify(model, input_shapes={'pixel_values': [BS, 3, H, W]})` first. This folds the mask into constants but bakes batch size, requiring per-batch ONNX + engine files. - **Secondary workaround**: lower `builder_optimization_level` to 2 via the Python TRT API (`config.builder_optimization_level = 2`). Prevents over-aggressive fusion; engines built this way are still compatible with `trtexec --loadEngine`. ### Dynamic Engine Batch-Size Anomalies (transformer models) Dynamic-shape engines for transformer models (DETR, RT-DETR) can show **non-monotonic throughput** — specific non-power-of-2 batch sizes (e.g., BS=17-19) perform dramatically worse than neighboring values. Cause: TRT tactic selection for attention layers at non-optimal shapes. When DS at `N` streams shows surprisingly low FPS, test `N±8` before concluding the GPU is saturated. Prefer power-of-2 batch sizes for production. ## Output Summary ```bash TOTAL_DURATION=$(python3 -c "print(round($STEP4_DURATION + $STEP5_DURATION, 2))") # bc is not in the container; python3 always is ``` When complete, print: ``` === TRT Engine Build Complete === Model: $MODEL_NAME Engine: models/$MODEL_NAME/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine (single engine — used for trtexec baseline and all DS runs) trtexec Results: BS=1: $QPS_BS1 QPS | GPU mean: ${GPU_MEAN_BS1}ms BS=$MAX_BS: $QPS_BS_MAX QPS | $IMGS_PER_SEC img/s | GPU mean: ${GPU_MEAN_BS_MAX}ms P99: ${GPU_P99_BS_MAX}ms PEAK_GPU_STREAMS (GPU-only upper bound): $PEAK_GPU_STREAMS streams @30fps Timing: Step 4 (engine build): ${STEP4_DURATION}s Step 5 (benchmarks): ${STEP5_DURATION}s Total Steps 4-5: ${TOTAL_DURATION}s Ready for: Steps 6-7 — read references/pipeline-run.md models/$MODEL_NAME/ ``` -
model-acquire.md 24.7 KB
# NV Model Acquire — Steps 1-3 Acquire an ONNX model from Hugging Face, creating the mandatory model folder structure. ## Intake — choose the model Before Step 1, present two explicit choices: 1. **Default model (recommended):** `PekingU/rtdetr_r50vd` from Hugging Face. 2. **Custom object-detection model:** collect a Hugging Face ID/URL or a versioned NVIDIA NGC catalog URL. Do not replace this with only an open-ended source prompt. If Default is selected, set `INPUT="PekingU/rtdetr_r50vd"`. If Custom is selected, require the source before continuing. Dry runs show the choices but perform no browsing, downloads, Docker launches, or file writes. ## MANDATORY: Model Folder Structure Create this layout at the start of Step 2 (once `$MODEL_NAME` is set by Step 1): ``` models/{model_name}/ model/ config/ parser/ scripts/ benchmarks/engines/ reports/charts/ samples/ ``` ```bash mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,reports/charts,samples} ``` Temporary staging dirs (`hf_model/`, `ngc_download/`, `build/`) are created inline where needed and cleaned up afterward — they are NOT part of this structure. ## Step 1: Parse the Model Source URL Accept a model URL or ID in one of these formats and extract the required fields: ```bash [ -z "$ARGUMENTS" ] && { echo "ERROR: No model URL or ID provided. Usage: /deepstream-import-vision-model <url>"; exit 1; } INPUT="${ARGUMENTS}" if echo "$INPUT" | grep -q "catalog.ngc.nvidia.com"; then # NGC catalog URL # e.g. https://catalog.ngc.nvidia.com/orgs/nvidia/teams/tao/models/trafficcamnet_transformer_lite/files?version=deployable_resnet50_v2.0 MODEL_SOURCE="ngc" NGC_ORG=$(echo "$INPUT" | sed 's|.*/orgs/\([^/]*\)/.*|\1|') NGC_TEAM=$(echo "$INPUT" | sed 's|.*/teams/\([^/]*\)/.*|\1|') MODEL_NAME=$(echo "$INPUT" | sed 's|.*/models/\([^/]*\)/.*|\1|') NGC_VERSION=$(echo "$INPUT" | sed 's|.*version=\([^&]*\).*|\1|') echo "Source: NGC Org: $NGC_ORG Team: $NGC_TEAM Model: $MODEL_NAME Version: $NGC_VERSION" else # HuggingFace full URL or short ID (e.g. https://huggingface.co/onnx-community/yolov8n or onnx-community/yolov8n) MODEL_SOURCE="hf" SLUG=$(echo "$INPUT" | sed 's|https://huggingface.co/||' | sed 's|/resolve/.*||' | sed 's|/$||') HF_ORG=$(echo "$SLUG" | cut -d/ -f1) MODEL_NAME=$(echo "$SLUG" | cut -d/ -f2) echo "Source: HF Org: $HF_ORG Model: $MODEL_NAME" fi ``` - `MODEL_SOURCE` (`hf` or `ngc`) drives category selection in Step 2 - `MODEL_NAME` is used as the folder name throughout (`models/{MODEL_NAME}/`) - Proceed to Step 2 with these variables set ## Step 2: Detect Model Source and Format First, create the model directory structure (required for all sources), then route by source: ```bash # Create permanent model directory structure (all sources — HF and NGC) mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,reports/charts,samples} # Route based on MODEL_SOURCE set in Step 1 if [ "$MODEL_SOURCE" = "ngc" ]; then echo "NGC model detected — skipping HF repo browse, proceeding to Step 2d" # Skip to Step 2d directly — do not run any HF curl commands below fi # The following HF browse, config download, and labels extraction only runs for MODEL_SOURCE=hf ``` - Browse the HF repository and classify available model files using the vetted helper script (validates inputs, uses HTTPS+TLSv1.2 only, honors `$HF_TOKEN`): ```bash FILES="$(bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-list-files.sh "$HF_ORG" "$MODEL_NAME")" ONNX_FILES=$(echo "$FILES" | grep -E '\.onnx$' || true) ST_FILES=$(echo "$FILES" | grep -E '\.(safetensors|bin)$' || true) echo "ONNX files: ${ONNX_FILES:-none}" echo "SafeTensors/bin: ${ST_FILES:-none}" echo "All files: $FILES" # If ONNX list is empty in root, also check /onnx subdirectory if [ -z "$ONNX_FILES" ]; then ONNX_SUB="$(bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-list-files.sh "$HF_ORG" "$MODEL_NAME" onnx | grep -E '\.onnx$' || true)" echo "ONNX in /onnx subdir: ${ONNX_SUB:-none}" fi ``` - Classify the repo into one of these categories: **Category A: ONNX files available** -> proceed to Step 2a (select ONNX variant) **Category B: SafeTensors/PyTorch only (no ONNX)** -> proceed to Step 2b (export to ONNX) **Category C: No usable model files** -> inform user, suggest alternative repos **Category D: NGC model (not on HuggingFace)** -> proceed to Step 2d (NGC download) - Download `config.json` — required for architecture detection and label extraction. Uses the vetted helper script (validated inputs, HTTPS+TLS, honors `$HF_TOKEN`): ```bash # HF: download from API via vetted helper. NGC: extracted from archive in Step 2d. if [ "$MODEL_SOURCE" = "hf" ]; then bash .claude/skills/deepstream-import-vision-model/scripts/model/hf-download-config.sh \ "$HF_ORG" "$MODEL_NAME" "models/$MODEL_NAME/config/config.json" else echo "NGC model — config.json will be extracted from the downloaded archive in Step 2d" fi # Note: models/$MODEL_NAME/config/ already exists from the MANDATORY mkdir at the top of Step 2 ``` - Inspect `config.json` to identify: - Model type (e.g., `grounding-dino`, `detr`, `yolos`, `resnet`, `swin`) - Architecture class (e.g., `GroundingDinoForObjectDetection`) - Number of inputs (single input vs multi-modal) - **Reject non-detection architectures (fail fast)**: Check the `architectures` field in `config.json` before continuing. If the architecture class ends in a non-detection suffix such as `ForImageClassification`, `ForSemanticSegmentation`, `ForInstanceSegmentation`, `ForPanopticSegmentation`, `ForDepthEstimation`, `ForMaskedLM`, `ForTokenClassification`, or `ForCausalLM`, **abort the pipeline with a clear error and exit non-zero**: `"deepstream-import-vision-model currently supports object detection models only. Detected architecture: {arch_class}. Classification, segmentation, and other vision tasks are not yet supported."` Do not prompt the user. Detection architectures end in `ForObjectDetection` (or, for some DETR-family variants, `ForConditionalDetection` / `ForZeroShotObjectDetection`). - **Validate the architecture and extract `labels.txt`** — one shared helper does both, so the HF and NGC routes cannot drift apart. Run it as soon as `config.json` is in place (for HF that is now; for NGC it runs at the end of Step 2d): ```bash build/.venv_optimum/bin/python \ .claude/skills/deepstream-import-vision-model/scripts/model/config-to-labels.py \ --config models/$MODEL_NAME/config/config.json \ --labels models/$MODEL_NAME/config/labels.txt ``` It exits non-zero on a non-detection architecture (the fail-fast gate above) and on a missing label map. **Treat either as fatal** — do not prompt the user, and never fall back to hardcoded COCO, ImageNet, or any other default list. ### Step 2a: Select ONNX Variant (Category A) - Identify available quantization variants (fp32, fp16, int8, int4, quantized, etc.) - **Default preference: fp16**. Apply this logic: 1. If fp16 variant exists -> **select it silently**, log: `"Selected: fp16 (default). All available: [list]"` 2. If fp16 does NOT exist -> **auto-select deterministically** in this priority order: fp32 > int8 > int4 > quantized > first ONNX alphabetically. Log: `"Selected: {variant} (fp16 unavailable). All available: [list]"`. Do not prompt the user. 3. If only one ONNX file exists -> log it and proceed without asking - **Construct the resolved download URL** for the selected variant from the tree listing: ```bash # The tree API returns entries with a "path" field (relative to repo root) # Construct the download URL as: PATH_FROM_TREE="<path field from tree listing, e.g. onnx/model_fp16.onnx>" ONNX_URL="https://huggingface.co/$HF_ORG/$MODEL_NAME/resolve/main/$PATH_FROM_TREE" # Example: path="onnx/model_fp16.onnx" -> URL ends in /resolve/main/onnx/model_fp16.onnx # Store this URL for use in Step 3 ``` - After URL construction, proceed to **Step 3** (download ONNX) ### Step 2b: Export SafeTensors to ONNX (Category B) When the repo only has `.safetensors` (or `.bin`) files and no ONNX export, convert to ONNX using an **isolated virtual environment** to avoid polluting the host system. #### 2b-i: Virtual Environment (already built by `setup.sh`, in-container) - The shared venv at **`build/.venv_optimum`** is created **inside the DeepStream container** by the skill's `setup.sh` (one-time bootstrap) — do **not** create it on the host. Every export command runs in-container against it; use `PY=build/.venv_optimum/bin/python` and `build/.venv_optimum/bin/optimum-cli`. - It's a single shared venv across all models (`torch`/`transformers`/`onnxruntime` are heavy and identical model-to-model). If it's missing, run the bootstrap (from SKILL.md Pre-flight): ```bash # in-container (via docker run … --entrypoint bash … setup.sh); NOT on the host .claude/skills/deepstream-import-vision-model/setup.sh # builds build/.venv_optimum + wkhtmltopdf ``` - For a new model that needs **extra packages** (e.g. `timm` for DETR-family backbones or `onnxsim`), `pip install` them **into the existing shared venv** rather than creating a new one: ```bash source build/.venv_optimum/bin/activate pip install timm # or: pip install onnxsim ``` - The venv lives under `build/.venv_optimum` at the repo root, keeping `models/` clean and excluded from git via the root `.gitignore` - All subsequent Python/pip commands in Step 2b must run inside this venv - Legacy per-model venvs at `build/.venv_$MODEL_NAME` from older runs are still cleaned up by `.claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME"` for backward compatibility #### 2b-ii: Download Required Files - Download from the HF repo into `models/$MODEL_NAME/hf_model/` using `-P` to avoid changing the working directory: ```bash mkdir -p models/$MODEL_NAME/hf_model HF_BASE="https://huggingface.co/$HF_ORG/$MODEL_NAME/resolve/main" # Download model files wget -P models/$MODEL_NAME/hf_model "$HF_BASE/model.safetensors" wget -P models/$MODEL_NAME/hf_model "$HF_BASE/config.json" wget -P models/$MODEL_NAME/hf_model "$HF_BASE/preprocessor_config.json" # For text+vision models, also download tokenizer files (failures are non-fatal): wget -P models/$MODEL_NAME/hf_model "$HF_BASE/tokenizer.json" || true wget -P models/$MODEL_NAME/hf_model "$HF_BASE/tokenizer_config.json" || true wget -P models/$MODEL_NAME/hf_model "$HF_BASE/vocab.txt" || true wget -P models/$MODEL_NAME/hf_model "$HF_BASE/special_tokens_map.json" || true ``` - For sharded models (multiple `.safetensors` files), also download `model.safetensors.index.json` and all shards #### 2b-iii: SafeTensors -> ONNX Export -- Max 3 Retries > **optimum was removed from this skill.** It pinned `transformers` below 4.54.0, which blocked the > releases fixing its RCE advisories (GHSA-29pf-2h5f-8g72, fixed in 5.3.0; GHSA-fgcw-684q-jj6r, fixed > in 5.5.0), and optimum 2.1.0 dropped the `onnx` subcommand entirely. Export now goes through > `torch.onnx.export` directly. - Run the export wrapper (it resolves the shared venv and calls `safetensors_to_onnx.py`): ```bash bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \ models/$MODEL_NAME/hf_model \ models/$MODEL_NAME/onnx_export/ ``` - It writes `models/$MODEL_NAME/onnx_export/model.onnx` and: - reads the export resolution from `preprocessor_config.json` (falling back to 640x640); - rejects non-detection architectures before doing any work; - **tries the dynamo backend, then falls back to TorchScript** if dynamo specialized the batch dimension — it prints `[export] backend=...` so you can see which one produced the graph; - consolidates any sidecar `model.onnx.data` back into the `.onnx`; - **verifies** the graph has the `pixel_values` input plus `logits` / `pred_boxes` outputs, and that the batch dimension really is dynamic. It exits non-zero rather than emitting a static-batch graph, because DeepStream needs `batch_size == num_streams` up to `MAX_BS`. - Expected output for the default model — note the automatic fallback: ``` [export] backend=dynamo [export] pixel_values shape=[2, 3, 640, 640] [export] dynamo produced a static batch dimension; trying the next backend [export] backend=legacy-torchscript [export] pixel_values shape=['batch', 3, 640, 640] ``` - Useful flags (passed straight through): `--opset N` · `--image-size N` · `--max-batch N` · `--static-batch` · `--legacy-torchscript` - If export succeeds, copy the ONNX file to the `model/` subdirectory: ```bash cp models/$MODEL_NAME/onnx_export/model.onnx models/$MODEL_NAME/model/$MODEL_NAME.onnx ``` - **Retry policy**: the two-backend fallback is automatic, so a failure here means both backends failed. Retry up to **3 times total** with adjustments between attempts: - **Retry 1**: Set `--image-size` explicitly if the error mentions a shape or resize mismatch - **Retry 2**: Try a different `--opset` (18 is the default; 17 and below fail on `Resize`) - **Retry 3**: Try `--static-batch` if both backends baked in the batch dimension. The engine must then be built at a fixed batch size — see `references/engine-build.md`. - After 3 failed attempts, fall back to **Step 2b-iv** (hand-written export for an unusual architecture) #### 2b-iv: Fallback -- Hand-written torch.onnx.export (unusual architectures) -- Max 3 Retries - If the wrapper fails after 3 retries because the model does not follow the standard `logits` / `pred_boxes` detection contract, write the export inline and adjust the wrapper outputs: ```bash build/.venv_optimum/bin/python -c " import torch from transformers import AutoModelForObjectDetection model = AutoModelForObjectDetection.from_pretrained('models/$MODEL_NAME/hf_model').eval() class Wrap(torch.nn.Module): def __init__(s, m): super().__init__(); s.m = m def forward(s, x): o = s.m(pixel_values=x) return o.logits, o.pred_boxes # <- adjust for the architecture # Dummy input matching preprocessor_config.json dimensions dummy = torch.randn(1, 3, 800, 800) # TorchScript backend — the more reliable one for a dynamic batch dimension on # DETR-family detectors. Swap to the dynamo block below if this backend fails. torch.onnx.export(Wrap(model), dummy, 'models/$MODEL_NAME/model/$MODEL_NAME.onnx', opset_version=18, do_constant_folding=True, dynamo=False, input_names=['pixel_values'], output_names=['logits', 'pred_boxes'], dynamic_axes={'pixel_values': {0: 'batch'}, 'logits': {0: 'batch'}, 'pred_boxes': {0: 'batch'}}) # dynamo backend (use for text-conditioned models the tracer cannot handle): # batch = torch.export.Dim('batch', min=1, max=16) # torch.onnx.export(Wrap(model), (torch.randn(2, 3, 800, 800),), '<out>.onnx', # opset_version=18, dynamo=True, # input_names=['pixel_values'], output_names=['logits', 'pred_boxes'], # dynamic_shapes={'pixel_values': {0: batch}}) " ``` - Adjust the wrapper's returned tensors, input/output names, and shapes for the architecture - Always confirm the batch dimension survived: ```bash build/.venv_optimum/bin/python -c " import onnx; d = onnx.load('models/$MODEL_NAME/model/$MODEL_NAME.onnx').graph.input[0].type.tensor_type.shape.dim print([x.dim_param or x.dim_value for x in d])" # expect ['batch', 3, H, W] ``` - **Retry policy**: If manual export fails, retry up to **3 times total** with adjustments: - **Retry 1**: Try a different `AutoModel` class, or return different tensors from the wrapper - **Retry 2**: Switch backend (TorchScript <-> dynamo), remembering `dynamic_axes` vs `dynamic_shapes` - **Retry 3**: Drop the dynamic batch entirely and build a fixed-batch engine - After 3 failed attempts, **stop and generate a failure report** > **Gotchas for recent PyTorch/transformers** (verified on torch 2.13.0 + transformers 5.14.1 > inside `nvcr.io/nvidia/deepstream:9.1-triton-multiarch`): > - **Neither backend works for every architecture — try `dynamo=True` first, fall back to `dynamo=False`.** `safetensors_to_onnx.py` does this automatically and reports which backend won. > - **`dynamo=True` can silently produce a STATIC batch dimension.** The model's own code may specialize `shape[0]`; `torch.export` then reports *"you marked batch as dynamic but your code specialized it to a constant"*. **RT-DETR (`PekingU/rtdetr_r50vd`, the default model) does exactly this** — dynamo yields `[2, 3, 640, 640]`, the TorchScript path yields `['batch', 3, 640, 640]`. Always verify with `onnx.load()`; the exporter asserts it. > - **`dynamo=False` does NOT crash on transformers 5.5+ for vision detectors.** An earlier revision of this document claimed it did, via `create_bidirectional_mask`. That applies to **text-conditioned** models (Grounding DINO and friends, which run a BERT text encoder) — not to pure-vision detectors. RT-DETR and YOLOS both export cleanly through TorchScript on transformers 5.14.1. > - Under `dynamo=True` the parameter is `dynamic_shapes` (with `torch.export.Dim`), **not** `dynamic_axes`; under `dynamo=False` it is `dynamic_axes`. Passing the wrong one is silently ineffective. > - **Use opset 18, not 17.** The dynamo exporter implements >= 18 and auto-upgrades, then its downgrade pass fails outright with `No Adapter To Version 17 for Resize`. Opset 18 is fine on TRT 10.16. > - **Call `.eval()` on the wrapper module too**, not just the loaded model — a fresh `nn.Module` defaults to training mode, which changes dropout/batchnorm behaviour during export. > - **External data files**: `torch.onnx.export` may produce `model.onnx.data` alongside the `.onnx`. Consolidate before TRT conversion: `m = onnx.load(path, load_external_data=True); onnx.save(m, consolidated_path)`. The exporter does this automatically. > - No `ForeignNode` failure was observed for RT-DETR on TRT 10.16 with either backend — see `references/engine-build.md` for the historical issue. #### 2b-v: Handle Multi-Modal Models (e.g., Grounding DINO) - Models that take **both image AND text** inputs need special handling for DeepStream (nvinfer only supports image input) - Strategy: **freeze the text prompt** into the ONNX graph as a constant 1. Run the model once with a fixed text prompt (e.g., "person . car . truck .") 2. Export ONNX with the text embeddings baked in as constants 3. The resulting ONNX model only needs `pixel_values` as input - If freezing is not possible, check `onnx-community/` for pre-converted single-input versions - **Inform the user** about the frozen text prompt and its implications (fixed detection classes) #### 2b-vi: onnxsim — Run After Export When Needed If the model has dynamic shape paths that cause TRT `ForeignNode` fusion issues, simplify the ONNX graph with `onnxsim` **before** engine building: ```bash source build/.venv_optimum/bin/activate pip install onnxsim python3 -m onnxsim \ models/$MODEL_NAME/model/$MODEL_NAME.onnx \ models/$MODEL_NAME/model/${MODEL_NAME}_sim.onnx # Use the _sim.onnx for engine building if the original triggers ForeignNode errors ``` Only run `onnxsim` if TRT build fails with `ForeignNode` warnings — it is not needed for most models. #### 2b-vii: Validate ONNX Output - After export, validate the ONNX file: ```bash source build/.venv_optimum/bin/activate python3 -c " import onnx m = onnx.load('models/$MODEL_NAME/model/$MODEL_NAME.onnx') onnx.checker.check_model(m) print('Inputs:') for i in m.graph.input: dims = [d.dim_param or d.dim_value for d in i.type.tensor_type.shape.dim] print(f' {i.name}: {dims}') print('Outputs:') for o in m.graph.output: dims = [d.dim_param or d.dim_value for d in o.type.tensor_type.shape.dim] print(f' {o.name}: {dims}') print('ONNX validation passed!') " ``` - Verify: - Single image input (no text/mask inputs -- remove if needed) - Output shapes match expected detection format - Dynamic batch dimension is present #### 2b-viii: Cleanup - Deactivate the venv after export is complete: ```bash deactivate ``` - **Keep `build/.venv_optimum` across runs** — it is shared by every SafeTensors → ONNX export and rebuilding it for each model costs minutes and GBs. `cleanup.sh` intentionally does not remove it. - `cleanup.sh` removes per-model artifacts (`models/$MODEL_NAME/hf_model`, `models/$MODEL_NAME/onnx_export`, and any legacy `build/.venv_$MODEL_NAME` left over from older runs): ```bash # Validated script; will refuse unsafe paths. Shared .venv_optimum is preserved. bash .claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME" # Preview without removing: # bash .claude/skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME" --dry-run ``` - The ONNX file is now at `models/$MODEL_NAME/model/$MODEL_NAME.onnx` -- proceed to engine building ### Step 2d: NGC Model Download (Category D) When the model comes from NVIDIA NGC (not HuggingFace), download using the `ngc` CLI if available, or fall back to `wget` for direct file download: ```bash # Vetted helper: prefers ngc CLI if installed, else falls back to authenticated # HTTPS+TLS via curl against the public NGC catalog API. All inputs validated # against ^[A-Za-z0-9._-]+$. See .claude/skills/deepstream-import-vision-model/scripts/model/ngc-download.sh for details. bash .claude/skills/deepstream-import-vision-model/scripts/model/ngc-download.sh \ "$NGC_ORG" "$NGC_TEAM" "$MODEL_NAME" "$NGC_VERSION" \ "models/$MODEL_NAME/ngc_download" # Inspect downloaded files echo "Downloaded files:" ls -lhR models/$MODEL_NAME/ngc_download/ ``` - Identify the ONNX file(s) in the downloaded archive (often inside a subdirectory named after the model version) - If the download contains a `.etlt` or `.engine` file only (TAO encrypted format), check if a plain ONNX is also provided; if not, use the TAO-provided engine directly and skip Step 4 (engine build) - Copy the ONNX to the model directory: ```bash NGC_ONNX=$(find models/$MODEL_NAME/ngc_download -name "*.onnx" | head -1) cp "$NGC_ONNX" models/$MODEL_NAME/model/$MODEL_NAME.onnx echo "ONNX: $NGC_ONNX -> models/$MODEL_NAME/model/$MODEL_NAME.onnx" ``` - Extract `config.json` from the archive and build `labels.txt` (same logic as HF path): ```bash NGC_CONFIG=$(find models/$MODEL_NAME/ngc_download -name "config.json" | head -1) if [ -z "$NGC_CONFIG" ]; then echo "ERROR: config.json not found in NGC archive — cannot create labels.txt" echo "Cannot proceed without a label map — aborting. Provide an NGC archive that contains config.json." exit 1 else cp "$NGC_CONFIG" models/$MODEL_NAME/config/config.json echo "config.json extracted from: $NGC_CONFIG" # Same helper the HF route uses: gates the architecture, then writes labels.txt. # The NGC route reaches config.json later than the HF route, so without this a # classification model would build an engine and a parser before anything noticed. build/.venv_optimum/bin/python \ .claude/skills/deepstream-import-vision-model/scripts/model/config-to-labels.py \ --config models/$MODEL_NAME/config/config.json \ --labels models/$MODEL_NAME/config/labels.txt || exit 1 fi ``` ## Step 3: Download the ONNX Model The model directory structure was already created in the MANDATORY block at the top. Do NOT run `mkdir -p` again here — just download the file: ```bash wget -O "models/$MODEL_NAME/model/$MODEL_NAME.onnx" "${ONNX_URL}" ``` Where `$ONNX_URL` is the resolved URL constructed at the end of Step 2a (Category A) or derived from the NGC download path (Category D). Categories B and D write the ONNX directly to `models/$MODEL_NAME/model/$MODEL_NAME.onnx` during export/copy — Step 3 only applies to Category A. - Also download any external data files if the ONNX model references them (files with `.onnx_data` extension or similar) - Verify the download completed successfully and report file size ## Timing Record wall-clock time at the start and end of this skill: ```bash STEP_START=$(date +%s.%N) # ... all steps ... STEP_END=$(date +%s.%N) STEP_DURATION=$(python3 -c "print(round($STEP_END - $STEP_START, 2))") # bc is not in the container; python3 always is ``` ## Output Summary When complete, print: ``` === HF Model Acquire Complete === [Steps 1-3: ${STEP_DURATION}s] Model: $MODEL_NAME ONNX: models/$MODEL_NAME/model/$MODEL_NAME.onnx ({size} MB) Input: {input_name} {input_shape} Output: {output_names} {output_shapes} Labels: {num_classes} classes -> models/$MODEL_NAME/config/labels.txt Ready for: Steps 4-5 — read references/engine-build.md models/$MODEL_NAME/model/$MODEL_NAME.onnx ``` (`{size}`, `{input_name}`, `{input_shape}`, `{output_names}`, `{output_shapes}`, `{num_classes}` are filled from the ONNX inspection output — all other fields use bash variables.) -
pipeline-run.md 27.2 KB
# DS Run Pipeline -- Steps 6-7 Integrate a TensorRT model into DeepStream with parser, validation, and multi-stream benchmarks. The model directory is: `$ARGUMENTS` ## Pre-flight: Extract Variables ```bash [ -z "$ARGUMENTS" ] && { echo "ERROR: No model directory provided. Usage: /deepstream-import-vision-model models/<model_name>/"; exit 1; } MODEL_DIR="${ARGUMENTS%/}" MODEL_NAME=$(basename "$MODEL_DIR") # Find ONNX file (exclude _dynamic variants created during export) ONNX_FILE=$(ls models/$MODEL_NAME/model/*.onnx 2>/dev/null | grep -v '_dynamic' | head -1) [ -z "$ONNX_FILE" ] && { echo "ERROR: No ONNX file found in models/$MODEL_NAME/model/ — run Steps 1-3 first (references/model-acquire.md)"; exit 1; } MODEL_FILENAME=$(basename "$ONNX_FILE" .onnx) # Engine + MAX_BS from the shared resolver (see scripts/model/resolve-engine.sh). eval "$(bash .claude/skills/deepstream-import-vision-model/scripts/model/resolve-engine.sh "$MODEL_NAME")" || exit 1 # Read PEAK_GPU_STREAMS from trtexec Step 5b log — fixed filename, no timestamp, no wildcard TRTEXEC_LOG="models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log" [ -f "$TRTEXEC_LOG" ] || { echo "ERROR: trtexec log not found at $TRTEXEC_LOG — run Steps 4-5 first (references/engine-build.md)"; exit 1; } QPS_BS_MAX=$(grep -oP 'Throughput:\s*\K[0-9.]+' "$TRTEXEC_LOG" | tail -1) read IMGS_PER_SEC PEAK_GPU_STREAMS < <(python3 -c " import math imgs = float('$QPS_BS_MAX') * $MAX_BS print(round(imgs, 2), int(math.floor(imgs / 30))) ") # Read spatial dimensions from ONNX inspection INSPECT_OUT=$(python3 .claude/skills/deepstream-import-vision-model/scripts/model/inspect-onnx.py "$ONNX_FILE") INPUT_NAME=$(echo "$INSPECT_OUT" | grep -oP 'input_name:\s*\K\S+') H=$(echo "$INSPECT_OUT" | grep -oP 'height:\s*\K[0-9]+') W=$(echo "$INSPECT_OUT" | grep -oP 'width:\s*\K[0-9]+') [ -z "$INPUT_NAME" ] && { echo "ERROR: could not parse INPUT_NAME from inspect output"; exit 1; } [ -z "$H" ] && { echo "ERROR: could not parse H — dynamic spatial dims? Set H manually"; exit 1; } [ -z "$W" ] && { echo "ERROR: could not parse W — dynamic spatial dims? Set W manually"; exit 1; } # Detect installed CUDA version for parser compilation CUDA_VER=$(ls /usr/local/ 2>/dev/null | grep -oP '^cuda-\K[0-9]+\.[0-9]+$' | sort -V | tail -1) [ -z "$CUDA_VER" ] && CUDA_VER=13.2 echo "CUDA_VER=$CUDA_VER" # Count labels [ -f "models/$MODEL_NAME/config/labels.txt" ] || { echo "ERROR: labels.txt not found — run Steps 1-3 first (references/model-acquire.md)"; exit 1; } NUM_LABELS=$(wc -l < models/$MODEL_NAME/config/labels.txt) # Parser function suffix: PascalCase of MODEL_NAME, sanitized for C++ identifiers # e.g. yolov8n→Yolov8n rtdetr-l→RtdetrL grounding-dino-base→GroundingDinoBase PARSER_FUNC_SUFFIX=$(python3 -c " import re parts = re.sub(r'[^a-zA-Z0-9]', ' ', '$MODEL_NAME').split() print(''.join(p.capitalize() for p in parts)) ") # Sanitize MODEL_NAME for use in C++ source/library filenames — mirrors PARSER_FUNC_SUFFIX logic. # e.g. rtdetr-l → rtdetr_l grounding-dino-base → grounding_dino_base # printf, NOT echo: echo appends a newline that `tr -c` turns into a trailing '_', yielding # e.g. `rtdetr_r50vd_` and a custom-lib-path that never matches the Makefile's TARGET_LIB. MODEL_NAME_SAFE=$(printf '%s' "$MODEL_NAME" | tr -c 'A-Za-z0-9' '_') # Video source — default is sample_720p.mp4 (MANDATORY). Never autonomously substitute # sample_1080p_h264.mp4 or any other file. DS_VIDEO may only be set when the user explicitly # provides a custom video path; it is not a licence to pick a different resolution. VIDEO="${DS_VIDEO:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}" [ -f "$VIDEO" ] || { echo "ERROR: Video file not found: $VIDEO" echo " Fix 1: Set DS_VIDEO=/path/to/sample_720p.mp4 before running" echo " Fix 2: Install DeepStream samples (DeepStream 9.1 samples): apt-get install deepstream-9.1-samples" exit 1 } echo "Model: $MODEL_NAME" echo "ONNX: $ONNX_FILE (input=$INPUT_NAME, ${H}x${W})" echo "Engine: $ENGINE (MAX_BS=$MAX_BS)" echo "PEAK_GPU_STREAMS: $PEAK_GPU_STREAMS (floor($IMGS_PER_SEC img/s / 30))" echo "Labels: $NUM_LABELS classes" ``` > All subsequent commands use these variables — never hardcoded paths or template placeholders. ## Step 6: DeepStream Integration ```bash STEP6_START=$(date +%s.%N) ``` ### 6a: Inspect Model Output Format Verify output tensor shapes and value ranges before writing the parser: ```bash python3 -c " import onnxruntime as ort, numpy as np sess = ort.InferenceSession('$ONNX_FILE') inp = sess.get_inputs()[0] out = sess.get_outputs() print(f'Input: {inp.name} shape={inp.shape}') for o in out: print(f'Output: {o.name} shape={o.shape}') dummy = np.random.randn(*[d if isinstance(d,int) else 1 for d in inp.shape]).astype(np.float32) result = sess.run(None, {inp.name: dummy}) for i,r in enumerate(result): print(f'Output[{i}] range: [{r.min():.4f}, {r.max():.4f}]') " ``` **CRITICAL**: Determine the correct `net-scale-factor` from the output ranges and model family: | Model expects | net-scale-factor | Notes | |---------------|-----------------|-------| | 0–255 input (OpenCV Zoo) | `1.0` | No normalization | | 0–1 normalized | `0.00392156862745098` (1/255) | Standard | | ImageNet normalized | `0.01752` + offsets | Rare in DS | Wrong scale factor = zero detections. Always verify with KITTI dump (Step 6g) before benchmarks. ### 6b: Write Custom Bounding Box Parser Create `models/$MODEL_NAME/parser/nvdsinfer_custombboxparser_${MODEL_NAME_SAFE}.cpp`: ```cpp extern "C" bool NvDsInferParseCustom${PARSER_FUNC_SUFFIX}( std::vector<NvDsInferLayerInfo> const &outputLayersInfo, NvDsInferNetworkInfo const &networkInfo, NvDsInferParseDetectionParams const &detectionParams, std::vector<NvDsInferObjectDetectionInfo> &objectList); CHECK_CUSTOM_PARSE_FUNC_PROTOTYPE(NvDsInferParseCustom${PARSER_FUNC_SUFFIX}); ``` Parser implementation rules: - Include `nvdsinfer_custom_impl.h` and use `NvDsInferObjectDetectionInfo` (classId, left, top, width, height, detectionConfidence) - Decode model-specific output format into pixel-space bounding boxes: - YOLOX-style `[N, num_anchors, 5+C]`: decode grid offsets, exp(w/h), objectness×class_score - SSD-style `[N, num_dets, 6]`: extract class, confidence, normalized → pixel coords - YOLO with BatchedNMS: parse keepCount, bboxes, scores, classes from 4 output layers - **Clip all coordinates** to `[0, networkInfo.width-1]` and `[0, networkInfo.height-1]` - Use `detectionParams.perClassPreclusterThreshold` for confidence filtering - **NMS**: Dense heads → `cluster-mode=2` (DeepStream NMS). Fused TRT NMS → `cluster-mode=4` - **Sanity check for undecoded output**: if bbox values land in [0, 3], the parser is reading grid-space offsets. Most models need `(raw + grid_offset) * stride` for cx/cy and `exp(raw) * stride` for w/h. Verify raw output ranges with Python/ONNX Runtime before writing the parser. - Reference: `/opt/nvidia/deepstream/deepstream/sources/libs/nvdsinfer_customparser/nvdsinfer_custombboxparser.cpp`; Header: `sources/includes/nvdsinfer_custom_impl.h` #### Model-family parser patterns - **DETR / Conditional DETR**: outputs `logits [B, num_queries, num_classes+1]` and `pred_boxes [B, num_queries, 4]`. Boxes are `(cx, cy, w, h)` normalized to `[0,1]` — convert to `(left, top, width, height)` in pixels. Use **softmax** (not sigmoid) on logits. **Background class is the LAST index** (e.g., index 91 for a 92-class DETR, despite `config.json` showing `"0": "N/A"`). Skip the background class when iterating. DETR uses Hungarian matching — NMS is not needed; set `cluster-mode=4` (not `nms-iou-threshold=0.0`, which is a legacy key). - **OWL-ViT / CLIP-based zero-shot detectors**: outputs `logits [B, num_patches, num_classes]` and `pred_boxes [B, num_patches, 4]`. **Sigmoid** activation (per-class independent scoring, not softmax). Boxes are `(cx, cy, w, h)` normalized `[0,1]`. Use `cluster-mode=2` (NMS with IoU threshold). CLIP preprocessing: `net-scale-factor=0.01459`, `offsets=122.77;116.75;104.09`. Confidence threshold 0.10 works well for general detection; lower to 0.05 for recall-focused tasks. - **HF RT-DETR preprocessing quirk**: `RTDetrImageProcessor` may have `do_normalize=false` even though `image_mean`/`image_std` fields exist. When `do_normalize=false`, the model expects `[0,1]` scaled input — set `net-scale-factor=1/255` with no offsets. The ONNX export does NOT bake normalization into the first Conv layer. Verify with ONNX Runtime on a real frame before debugging nvinfer. #### NGC TAO models — use the built-in parser library NVIDIA NGC TAO models (trafficcamnet, peoplenet, TrafficCamNet Transformer Lite, etc.) ship with TAO-specific parsers pre-compiled into a system library: - **Library path**: `/opt/nvidia/deepstream/deepstream/lib/libnvds_infercustomparser.so` — NOT `libnvds_infercustomparser_tao.so` (even if the NGC YAML config suggests it). - Custom parse function names: `NvDsInferParseCustomDDETRTAO`, `NvDsInferParseCustomRTDETRTAO`, etc. - **No custom parser compilation needed** — point `custom-lib-path` at the system library and `parse-bbox-func-name` at the TAO function. - KITTI dump from `deepstream-app` may emit zero-valued bbox coordinates for DETR/RT-DETR parsers even when detections are correct. Verify visually with JPEG frame extraction instead. ### `network-type` vs `model-type` — use `network-type=0` - `model-type` is a legacy/unknown key — nvinfer ignores it with a warning. - `network-type=0` (Detector) is required to invoke `parse-bbox-func-name`. - `network-type=100` (Other) does NOT invoke the custom bbox parser — it requires `output-tensor-meta=1` for external post-processing. - **Symptom of the wrong key**: custom parse function is never called (zero detections, no parser debug output) — check that `network-type=0` is set. ### 6c: Create Makefile Write `models/$MODEL_NAME/parser/Makefile` using Python to guarantee literal TAB characters in recipe lines (heredoc in bash can produce spaces, which break make): ```bash python3 - << EOF model = '$MODEL_NAME' model_safe = '$MODEL_NAME_SAFE' content = ( "DEEPSTREAM_DIR ?= /opt/nvidia/deepstream/deepstream\n" "CUDA_VER ?= 12.8\n" "CC := g++\n" "CFLAGS := -Wall -std=c++11 -shared -fPIC\n" "CFLAGS += -I\$(DEEPSTREAM_DIR)/sources/includes -I/usr/local/cuda-\$(CUDA_VER)/include\n" "LIBS := -lnvinfer\n" "LFLAGS := -Wl,--start-group \$(LIBS) -Wl,--end-group\n" f"SRCFILES := nvdsinfer_custombboxparser_{model_safe}.cpp\n" f"TARGET_LIB := libnvdsinfer_{model_safe}_parser.so\n" "\n" "all: \$(TARGET_LIB)\n" "\$(TARGET_LIB): \$(SRCFILES)\n" "\t\$(CC) -o \$@ \$^ \$(CFLAGS) \$(LFLAGS)\n" # TAB required by make "clean:\n" "\trm -rf \$(TARGET_LIB)\n" # TAB required by make ) with open(f'models/{model}/parser/Makefile', 'w') as f: f.write(content) print(f"Makefile written: models/{model}/parser/Makefile") EOF ``` ### 6d: Build Parser Library ```bash make -C models/$MODEL_NAME/parser \ DEEPSTREAM_DIR=/opt/nvidia/deepstream/deepstream \ CUDA_VER=$CUDA_VER # Verify the symbol is exported nm -D models/$MODEL_NAME/parser/libnvdsinfer_${MODEL_NAME_SAFE}_parser.so | grep NvDsInferParseCustom ``` ### 6e: Create nvinfer Config File ```bash cat > models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt << EOF [property] gpu-id=0 net-scale-factor=0.00392156862745098 model-color-format=0 onnx-file=../model/${MODEL_FILENAME}.onnx model-engine-file=../benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine labelfile-path=labels.txt batch-size=1 network-mode=2 num-detected-classes=${NUM_LABELS} process-mode=1 interval=0 gie-unique-id=1 network-type=0 custom-lib-path=../parser/libnvdsinfer_${MODEL_NAME_SAFE}_parser.so parse-bbox-func-name=NvDsInferParseCustom${PARSER_FUNC_SUFFIX} # 2=DeepStream NMS (dense heads: YOLO, SSD). Use 4 if engine has fused NMS output cluster-mode=2 infer-dims=3;${H};${W} maintain-aspect-ratio=1 [class-attrs-all] topk=200 nms-iou-threshold=0.45 pre-cluster-threshold=0.25 EOF ``` > **Path note**: All paths are relative to the `config/` directory where this file lives. > `net-scale-factor` defaults to `1/255` — update to `1.0` if the model expects 0–255 input (verify via Step 6a). Verify label count matches: ```bash echo "labels.txt: $NUM_LABELS classes -> num-detected-classes=$NUM_LABELS" ``` ### 6f: Single-Stream Visual Validation > **ENCODER RULE:** > Primary encoder is `nvv4l2h264enc` (NVENC via V4L2) → `.mp4`. `x264enc` and `openh264enc` are **prohibited**. > On systems where `/dev/v4l2-nvenc` is unavailable, the approved fallback is `theoraenc + oggmux` > (LGPL; both ship in gst-plugins-base) → `.ogv`. If `theoraenc`/`oggmux` are absent, video creation is skipped. > Use `.claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-single-stream.sh` which handles this automatically > and emits a `DS_SINGLE_STREAM_MODE=` marker the report parser reads. **Primary (NVENC available):** ```bash mkdir -p models/$MODEL_NAME/samples GST_DEBUG=1 gst-launch-1.0 \ filesrc location=$VIDEO ! \ qtdemux ! queue leaky=downstream ! h264parse ! queue ! nvv4l2decoder ! queue ! \ m.sink_0 nvstreammux name=m batch-size=1 width=1280 height=720 ! queue ! \ nvinfer config-file-path=models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt ! queue ! \ nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! \ nvdsosd ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! \ nvv4l2h264enc ! h264parse ! mp4mux ! \ filesink location=models/$MODEL_NAME/samples/${MODEL_NAME}_output.mp4 sync=0 ``` **Fallback (NVENC unavailable — `/dev/v4l2-nvenc` missing, `theoraenc`/`oggmux` present):** Output extension switches from `.mp4` to `.ogv` (Ogg/Theora container). `theoraenc` consumes planar `I420`, not `NV12`. ```bash GST_DEBUG=1 gst-launch-1.0 \ filesrc location=$VIDEO ! \ qtdemux ! queue leaky=downstream ! h264parse ! queue ! nvv4l2decoder ! queue ! \ m.sink_0 nvstreammux name=m batch-size=1 width=1280 height=720 ! queue ! \ nvinfer config-file-path=models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt ! queue ! \ nvvideoconvert ! nvdsosd ! nvvideoconvert ! \ "video/x-raw, format=I420" ! theoraenc quality=48 ! oggmux ! \ filesink location=models/$MODEL_NAME/samples/${MODEL_NAME}_output.ogv sync=0 ``` Extract a frame to visually confirm bounding boxes — auto-detect which output file exists: ```bash SAMPLE_OUT=$(ls models/$MODEL_NAME/samples/${MODEL_NAME}_output.{mp4,ogv} 2>/dev/null | head -1) case "$SAMPLE_OUT" in *.mp4) gst-launch-1.0 \ filesrc location="$SAMPLE_OUT" ! \ qtdemux ! h264parse ! nvv4l2decoder ! videoconvert ! "video/x-raw,format=RGB" ! \ jpegenc quality=95 ! \ multifilesink location=models/$MODEL_NAME/samples/frame_%04d.jpg max-files=3 ;; *.ogv) gst-launch-1.0 \ filesrc location="$SAMPLE_OUT" ! \ oggdemux ! theoradec ! videoconvert ! "video/x-raw,format=RGB" ! \ jpegenc quality=95 ! \ multifilesink location=models/$MODEL_NAME/samples/frame_%04d.jpg max-files=3 ;; esac ``` If **no detections appear**, the most common cause is wrong `net-scale-factor` — update the config and re-run. ### 6g: KITTI Dump — Verify Detections Programmatically Run a KITTI dump to confirm detections exist before multi-stream benchmarks. > **Note:** `gie-kitti-output-dir` is a `deepstream-app` `[application]` > property — it is **not** read by `nvinfer` directly. Appending it to the > nvinfer config and running a `gst-launch-1.0 ... nvinfer ...` pipeline > silently produces zero KITTI files. Use the `ds-kitti-dump.sh` helper, > which wraps `deepstream-app` with the correct `[application]` section. ```bash mkdir -p models/$MODEL_NAME/samples/kitti_output bash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-kitti-dump.sh \ models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt \ models/$MODEL_NAME/samples/kitti_output \ 100 \ "$VIDEO" # Summarise detection results KITTI_FILES=$(ls models/$MODEL_NAME/samples/kitti_output/*.txt 2>/dev/null | wc -l) echo "KITTI frames written: $KITTI_FILES" echo "Top detected classes:" cat models/$MODEL_NAME/samples/kitti_output/*.txt 2>/dev/null \ | awk '{print $1}' | sort | uniq -c | sort -rn | head -10 ``` **Validation gate**: If `KITTI_FILES == 0` or all files are empty, detections are broken. Do NOT proceed to Step 7. ```bash # MANDATORY hard stop — do not comment out or remove this check if [ "$KITTI_FILES" -eq 0 ]; then echo "ERROR: KITTI validation FAILED — zero detection files written." echo "Fix net-scale-factor, parser output format, or config before retrying." echo "Do NOT proceed to Step 7 benchmarks with broken detections." exit 1 fi FRAMES_WITH_DETECTIONS=$(grep -rl '.' models/$MODEL_NAME/samples/kitti_output/ 2>/dev/null | wc -l) DETECTION_RATE=$(python3 -c "print(round($FRAMES_WITH_DETECTIONS/$KITTI_FILES*100,1))") echo "Detection rate: $FRAMES_WITH_DETECTIONS / $KITTI_FILES frames = ${DETECTION_RATE}%" if python3 -c "exit(0 if $FRAMES_WITH_DETECTIONS/$KITTI_FILES >= 0.9 else 1)"; then echo "KITTI validation PASSED (>= 90% frames with detections)" else echo "ERROR: Detection rate ${DETECTION_RATE}% < 90% threshold. Fix parser before proceeding." exit 1 fi ``` ```bash STEP6_END=$(date +%s.%N) STEP6_DURATION=$(python3 -c "print(round($STEP6_END - $STEP6_START, 2))") # bc is not in the container; python3 always is echo "[Step 6] completed in ${STEP6_DURATION}s" ``` ### DeepStream Troubleshooting | Symptom | Fix | |---------|-----| | Zero detections | Wrong `net-scale-factor` — check model family table in Step 6a | | Engine rebuilds every run | `model-engine-file` path wrong — verify relative path from `config/` | | Parser crash | Output tensor shape mismatch — re-check Step 6a output shapes | | Wrong bounding box positions | Grid/stride decoding mismatch — verify model architecture docs | | `"layers num: 0"` | Harmless for dynamic-shape engines — do not debug | | deepstream-app segfaults | Use `gst-launch-1.0` instead (transformer models) | ## Step 7: Multi-Stream DeepStream Benchmark ### 7b: Create DS Benchmark Config Create one nvinfer config for all DS benchmark runs. `batch-size` is overridden at runtime via the nvinfer GStreamer element property. This config is **derived from the Step 6e config** rather than re-authored — every `[property]` and `[class-attrs-all]` value is identical. Only the relative paths and `batch-size` differ, because this copy lives one directory deeper (`benchmarks/ds/` instead of `config/`). Deriving it keeps the two from drifting apart: ```bash mkdir -p models/$MODEL_NAME/benchmarks/ds sed -e 's#^onnx-file=\.\./#onnx-file=../../#' \ -e 's#^model-engine-file=\.\./benchmarks/engines/#model-engine-file=../engines/#' \ -e 's#^labelfile-path=labels\.txt#labelfile-path=../../config/labels.txt#' \ -e "s#^batch-size=1\$#batch-size=${MAX_BS}#" \ -e 's#^custom-lib-path=\.\./#custom-lib-path=../../#' \ models/$MODEL_NAME/config/config_infer_primary_${MODEL_NAME}.txt \ > models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt ``` The five derived keys, for reference: | Key | Step 6e (`config/`) | Step 7b (`benchmarks/ds/`) | |---|---|---| | `onnx-file` | `../model/…` | `../../model/…` | | `model-engine-file` | `../benchmarks/engines/…` | `../engines/…` | | `labelfile-path` | `labels.txt` | `../../config/labels.txt` | | `batch-size` | `1` | `${MAX_BS}` | | `custom-lib-path` | `../parser/…` | `../../parser/…` | > **Path note**: Paths are relative to `benchmarks/ds/` where this config lives. ### Queue Placement Rules (MANDATORY) Every pipeline stage must be separated by `queue` elements. Use `leaky=downstream` after `qtdemux` to drop excess frames under GPU saturation; all other queues use no leaky setting (threading only). Always set `batched-push-timeout=-1` on `nvstreammux`. **Never include** `nvmultistreamtiler`, `nvdsosd`, or extra `nvvideoconvert` in benchmark runs — only use for single-stream visual validation (Step 6f). ### 7c: Two-Run DS Benchmark Only **2 DS pipeline runs** characterise DS overhead vs trtexec. Both runs go through `deepstream-app` with `[application] enable-perf-measurement=1` (wrapped by `.claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh`). FPS is parsed from the canonical `**PERF:` lines DeepStream emits at the configured measurement interval. This replaces the older `gst-launch-1.0 ... ! fpsdisplaysink` path so the runtime no longer depends on `gstreamer1.0-plugins-bad`. > **PERF line format**: `**PERF: <fps_run> (<fps_avg>)` — one float per active source. The helper script averages the per-stream instantaneous FPS across the last few measurement windows; the parser below mirrors that contract. **DS Run 1 — Calibration at PEAK_GPU_STREAMS streams:** > **CRITICAL**: Use `$PEAK_GPU_STREAMS` directly. Do NOT pre-apply any efficiency discount (no ×0.6, ×0.7, etc.). Run 1 *measures* the real overhead — do not guess it. > Log filenames are **fixed** — no timestamp variation. Always `ds_s${N}_run1.log` and `ds_s${N}_run2.log` in `benchmarks/ds/`. The report-generation skill reads these exact paths. ```bash # Hard constraint: num_streams <= engine max batch size — always N=$(python3 -c "print(min($PEAK_GPU_STREAMS, $MAX_BS))") LOG_RUN1="models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run1.log" STEP7_RUN1_START=$(date +%s.%N) bash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh \ models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt \ "$N" \ "$LOG_RUN1" \ "$VIDEO" FPS_RUN1=$(grep -oP '\*\*PERF:\s*\K[0-9.]+' "$LOG_RUN1" | tail -10 | python3 -c " import sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)") python3 -c "exit(0 if float('$FPS_RUN1') > 0 else 1)" || \ { echo "ERROR: FPS parsing failed for Run 1 — check $LOG_RUN1"; exit 1; } TOTAL_FPS_RUN1=$(python3 -c "print(round(float('$FPS_RUN1') * $N, 2))") RT_STREAMS=$(python3 -c "import math; print(min(int(math.floor(float('$TOTAL_FPS_RUN1') / 30)), $MAX_BS))") echo "DS Run 1: $N streams | FPS/stream=$FPS_RUN1 | total=$TOTAL_FPS_RUN1 img/s | RT_STREAMS=$RT_STREAMS" STEP7_RUN1_END=$(date +%s.%N) STEP7_RUN1_DURATION=$(python3 -c "print(round($STEP7_RUN1_END - $STEP7_RUN1_START, 2))") # bc is not in the container; python3 always is echo "[Step 7 Run 1] completed in ${STEP7_RUN1_DURATION}s" ``` **DS Run 2 — Validation at RT_STREAMS:** ```bash N=$RT_STREAMS LOG_RUN2="models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run2.log" STEP7_RUN2_START=$(date +%s.%N) bash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh \ models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt \ "$N" \ "$LOG_RUN2" \ "$VIDEO" FPS_RUN2=$(grep -oP '\*\*PERF:\s*\K[0-9.]+' "$LOG_RUN2" | tail -10 | python3 -c " import sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)") python3 -c "exit(0 if float('$FPS_RUN2') > 0 else 1)" || \ { echo "ERROR: FPS parsing failed for Run 2 — check $LOG_RUN2"; exit 1; } TOTAL_FPS_RUN2=$(python3 -c "print(round(float('$FPS_RUN2') * $N, 2))") RT_CONFIRMED=$(python3 -c "print('YES' if float('$FPS_RUN2') >= 30 else 'NO')") echo "DS Run 2: $N streams | FPS/stream=$FPS_RUN2 | total=$TOTAL_FPS_RUN2 img/s | Real-time: $RT_CONFIRMED" STEP7_RUN2_END=$(date +%s.%N) STEP7_RUN2_DURATION=$(python3 -c "print(round($STEP7_RUN2_END - $STEP7_RUN2_START, 2))") # bc is not in the container; python3 always is echo "[Step 7 Run 2] completed in ${STEP7_RUN2_DURATION}s" ``` > **NVDEC saturation on fast nano models**: very fast models (YOLO-nano family, etc.) can saturate NVDEC before GPU. Symptom: DS aggregate FPS plateaus at the same value regardless of stream count (e.g., 6,976 at 128 streams, 7,060 at 200 streams). In this case, `PEAK_GPU_STREAMS` from trtexec is an overestimate — Run 1 at that count will show fps/stream well below 30. The `RT_STREAMS = floor(TOTAL_FPS_RUN1 / 30)` formula above produces the correct NVDEC-limited ceiling. Do not pre-apply an efficiency factor to `PEAK_GPU_STREAMS` to compensate — the 2-run method measures overhead, it does not guess it. **If Run 2 is still not real-time** (FPS/stream < 30): **converge to the true ceiling — do NOT halve.** > **Why not halve:** halving overshoots badly. If Run 2 at 38 streams measures 29.6 fps/stream > (total 1124.8 img/s — only 1.3% under real-time), the real ceiling is ~37 streams, not 19. > Halving to 19 discards ~half the GPU's real capacity and reports a misleadingly low number. > Instead, recompute the target from Run 2's *measured* total throughput > (`floor(TOTAL_FPS_RUN2 / 30)`), which is strictly below the count that just failed, then > step down by 1 until real-time. This lands on the true ceiling in 1–2 short retries. ```bash # Bounded convergence: recompute from measured throughput, then decrement by 1. RETRIES=0 while [ "$RT_CONFIRMED" = "NO" ] && [ "$RETRIES" -lt 5 ]; do # First correction: jump to floor(measured_total/30); afterwards step down by 1. NEXT=$(python3 -c "import math; print(int(math.floor(float('$TOTAL_FPS_RUN2') / 30)))") [ "$NEXT" -ge "$N" ] && NEXT=$((N - 1)) # guarantee strict progress below the failing count RT_STREAMS=$(python3 -c "print(max(1, $NEXT))") N=$RT_STREAMS echo "Run 2 not real-time (fps/stream=$FPS_RUN2) — converging to $N streams" LOG_RUN2="models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run2.log" bash .claude/skills/deepstream-import-vision-model/scripts/deepstream/ds-perf-run.sh \ models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${MODEL_NAME}.txt \ "$N" \ "$LOG_RUN2" \ "$VIDEO" FPS_RUN2=$(grep -oP '\*\*PERF:\s*\K[0-9.]+' "$LOG_RUN2" | tail -10 | python3 -c " import sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)") TOTAL_FPS_RUN2=$(python3 -c "print(round(float('$FPS_RUN2') * $N, 2))") RT_CONFIRMED=$(python3 -c "print('YES' if float('$FPS_RUN2') >= 30 else 'NO')") RETRIES=$((RETRIES + 1)) echo "Retry $RETRIES: $N streams | FPS/stream=$FPS_RUN2 | Real-time: $RT_CONFIRMED" [ "$N" -le 1 ] && break done ``` **CONSTRAINT**: `num_streams <= engine_max_bs` always. Already enforced above via `min(RT_STREAMS, MAX_BS)`. ```bash TRTEXEC_QPS=$(grep -oP 'Throughput:\s*\K[0-9.]+' "$TRTEXEC_LOG" | tail -1) TRTEXEC_IMGS=$(python3 -c "print(round(float('$TRTEXEC_QPS') * $MAX_BS, 2))") DS_EFF_RUN1=$(python3 -c "print(round(float('$TOTAL_FPS_RUN1') / float('$TRTEXEC_IMGS') * 100, 1))") DS_EFF_RUN2=$(python3 -c "print(round(float('$TOTAL_FPS_RUN2') / float('$TRTEXEC_IMGS') * 100, 1))") ``` ## Timing and Output Summary ```bash TOTAL_67_DURATION=$(python3 -c "print(round($STEP6_DURATION + $STEP7_RUN1_DURATION + $STEP7_RUN2_DURATION, 2))") # bc is not in the container; python3 always is ``` When complete, print: ``` === DeepStream Integration Complete === Model: $MODEL_NAME | Engine: $ENGINE trtexec: $TRTEXEC_IMGS img/s @ BS=$MAX_BS DS Run 1 (PEAK): $PEAK_GPU_STREAMS streams | $FPS_RUN1 fps/s | eff $DS_EFF_RUN1% DS Run 2 (RT): $RT_STREAMS streams | $FPS_RUN2 fps/s | RT: $RT_CONFIRMED | eff $DS_EFF_RUN2% Timing: Step6=${STEP6_DURATION}s Run1=${STEP7_RUN1_DURATION}s Run2=${STEP7_RUN2_DURATION}s Total=${TOTAL_67_DURATION}s Ready for: Step 8 — read references/report-generation.md models/$MODEL_NAME/ ``` -
README.md 774 B
# deepstream-import-vision-model — Reference Documents Detailed phase guides for the `deepstream-import-vision-model` skill. Read the relevant file before starting each pipeline phase. | Document | Pipeline Steps | When to read | |---|---|---| | [model-acquire.md](model-acquire.md) | Steps 1–3 | Downloading from HuggingFace or NGC; ONNX vs SafeTensors detection and export | | [engine-build.md](engine-build.md) | Steps 4–5 | TensorRT dynamic engine build; `trtexec` BS=1 and BS=MAX\_BS benchmarks | | [pipeline-run.md](pipeline-run.md) | Steps 6–7 | Custom `nvinfer` bbox parser; single-stream validation; multi-stream benchmark sweep | | [report-generation.md](report-generation.md) | Step 8 | 5 benchmark charts; Markdown → HTML → PDF report generation | -
report-generation.md 26.8 KB
# NV Import Vision Model Report -- Step 8 Generate benchmark report with charts, HTML, and PDF from completed benchmarks. The model directory is: `$ARGUMENTS` > ## ⛔ STRICT HTML+PDF RULE — NO EXCEPTIONS, NO DEVIATIONS > > **HTML and PDF MUST be generated via the canonical pipeline script. Do NOT write your own HTML generator.** > > **The ONLY permitted way to generate the HTML + PDF:** > ```bash > python3 .claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py \ > models/$MODEL_NAME/reports/benchmark_report.md \ > .claude/skills/deepstream-import-vision-model/scripts/report/report-style.css \ > models/$MODEL_NAME/reports/ \ > $MODEL_NAME > ``` > This produces: > - `models/$MODEL_NAME/reports/benchmark_report.html` — styled with report_style.css, charts embedded as base64 > - `models/$MODEL_NAME/reports/benchmark_report_${MODEL_NAME}.pdf` — via wkhtmltopdf > > **FORBIDDEN — never do any of these:** > - Write your own `generate_html.py` or any custom markdown-to-HTML converter script > - Call `wkhtmltopdf` directly — use `md-to-html-pdf.py` which already calls it correctly > - Use `md-to-pdf.sh` — GFM+Mermaid design doc tool only, wrong CSS > - Use `pandoc`, `pdflatex`, or any other converter > > The `report_style.css` provides the ONLY correct CSS (dark navy headers #283593, alternating rows #e8eaf6, dark code blocks #263238). Any other CSS produces wrong-looking reports. ## 8a: Report Structure — 12 Mandatory Sections The report must contain exactly these 12 sections in order: 1. **Model Configuration** — model name, source (HF repo / NGC), architecture, ONNX source, input/output shapes, classes, custom parser name, cluster mode, precision, engine profile 2. **System Configuration** — GPU (name + VRAM), Driver, CUDA, TensorRT, DeepStream, OS, Python, PyTorch, ONNX versions 3. **Preprocessing** — net-scale-factor, offsets, color format, normalization details (with reference to the preprocessing table in deepstream-import-vision-model/SKILL.md) 4. **Engine Build Summary** — source format, conversion path, engine filename (with max_bs postfix), engine size (MB), FP16 flag, builder_optimization_level if non-default, timing cache path 5. **trtexec Results** — two runs (BS=1 and BS=MAX_BS) with: QPS, Images/s, GPU Compute mean/P99 (ms). Do NOT include H2D/D2H latency or Host Latency. Show PEAK_GPU_STREAMS derivation: ``` PEAK_GPU_STREAMS = floor(QPS_at_MAX_BS × MAX_BS / 30) = floor(imgs_per_sec_at_MAX_BS / 30) ``` 6. **PEAK_GPU_STREAMS Derivation** — explicit calculation block showing formula, inputs, and result. If a second engine was built, show both PEAK_GPU_STREAMS computations. 7. **Single-Stream Validation** — KITTI frame count, frames with detections, top-10 detected classes (from KITTI dump), validation result (PASS/FAIL) 8. **DeepStream Benchmark Results** — two runs: - **DS Run 1 (Calibration at PEAK_GPU_STREAMS)**: streams, batch, FPS/stream, total img/s, real-time (YES/NO) - **DS Run 2 (Validation at RT_STREAMS)**: streams, batch, FPS/stream, total img/s, real-time (YES) 9. **trtexec vs DeepStream Comparison** — 3-column table: trtexec | DS Run 1 | DS Run 2, rows: engine, batch/streams, total imgs/s, FPS/stream, real-time ≥30fps, DS Efficiency % 10. **Efficiency Analysis** — efficiency formula, Run 1 and Run 2 percentages, breakdown of the gap (NVDEC + mux + GStreamer overhead), GPU-bound vs pipeline-bound verdict 11. **Pipeline Timing** — per-step wall-clock duration and total: | Step | Description | Duration | |------|-------------|----------| | 1-3 | HF Model Acquire (download + inspect ONNX) | {time}s | | 4 | Engine build | {time}s | | 5 | trtexec BS=1 + BS=MAX_BS | {time}s | | 6 | Parser + config + visual validation + KITTI | {time}s | | 7 Run 1 | DS Calibration (PEAK_GPU_STREAMS streams) | {time}s | | 7 Run 2 | DS Validation (RT_STREAMS streams) | {time}s | | 8 | Report generation | {time}s | | **Total** | **End-to-end** | **{total}s** | 12. **Reference Commands** — exact reproducible commands: - trtexec engine build (full command with all flags and paths) - trtexec benchmark BS=1 and BS=MAX_BS - DeepStream single-stream validation (`gst-launch-1.0` with filesink + OSD) - DeepStream multi-stream benchmark (`deepstream-app` with `enable-perf-measurement=1` via `ds-perf-run.sh`, PEAK_GPU_STREAMS and RT_STREAMS variants) - nvinfer config key fields (as an ini code block) - Custom parser build command (`make` with DEEPSTREAM_DIR and CUDA_VER) - Use actual absolute paths from the model directory, never placeholders ## Pre-flight: Extract Variables from Benchmark Logs Before generating any output, derive all variables by reading completed benchmark files. These variables are used by every section below. ```bash STEP8_START=$(date +%s.%N) MODEL_DIR="${ARGUMENTS%/}" MODEL_NAME=$(basename "$MODEL_DIR") # Engine + MAX_BS + MODEL_FILENAME from the shared resolver (scripts/model/resolve-engine.sh). eval "$(bash .claude/skills/deepstream-import-vision-model/scripts/model/resolve-engine.sh "$MODEL_NAME")" || exit 1 # Extract input name and spatial dims from ONNX (needed for reference commands in the report) ONNX_FILE=$(ls models/$MODEL_NAME/model/*.onnx 2>/dev/null | grep -v '_dynamic' | head -1) if [ -n "$ONNX_FILE" ]; then INSPECT_OUT=$(python3 .claude/skills/deepstream-import-vision-model/scripts/model/inspect-onnx.py "$ONNX_FILE" 2>/dev/null) INPUT_NAME=$(echo "$INSPECT_OUT" | grep -oP 'input_name:\s*\K\S+') H=$(echo "$INSPECT_OUT" | grep -oP 'height:\s*\K[0-9]+') W=$(echo "$INSPECT_OUT" | grep -oP 'width:\s*\K[0-9]+') fi INPUT_NAME=${INPUT_NAME:-"images"} # fallback H=${H:-"640"}; W=${W:-"640"} # fallback — update if model uses different resolution # Parse trtexec BS=1 log — fixed filename trtexec_b1.log (no timestamp, no wildcard needed) TRTEXEC_LOG_BS1="models/$MODEL_NAME/benchmarks/b1/trtexec_b1.log" [ -f "$TRTEXEC_LOG_BS1" ] || { echo "ERROR: $TRTEXEC_LOG_BS1 not found — run Steps 4-5 first (references/engine-build.md)"; exit 1; } QPS_BS1=$(grep -oP 'Throughput:\s*\K[0-9.]+' "$TRTEXEC_LOG_BS1" | tail -1) GPU_MEAN_BS1=$(grep -oP 'GPU Compute Time:.*mean = \K[0-9.]+' "$TRTEXEC_LOG_BS1" | tail -1) # Parse trtexec BS=MAX_BS log — fixed filename trtexec_b${MAX_BS}.log TRTEXEC_LOG_BSMAX="models/$MODEL_NAME/benchmarks/b${MAX_BS}/trtexec_b${MAX_BS}.log" [ -f "$TRTEXEC_LOG_BSMAX" ] || { echo "ERROR: $TRTEXEC_LOG_BSMAX not found — run Steps 4-5 first (references/engine-build.md)"; exit 1; } QPS_BS_MAX=$(grep -oP 'Throughput:\s*\K[0-9.]+' "$TRTEXEC_LOG_BSMAX" | tail -1) GPU_MEAN_BS_MAX=$(grep -oP 'GPU Compute Time:.*mean = \K[0-9.]+' "$TRTEXEC_LOG_BSMAX" | tail -1) GPU_P99_BS_MAX=$(grep -oP 'GPU Compute Time:.*percentile\(99%\) = \K[0-9.]+' "$TRTEXEC_LOG_BSMAX" | tail -1) [ -z "$QPS_BS_MAX" ] && { echo "ERROR: Could not parse Throughput from $TRTEXEC_LOG_BSMAX — log may be empty or malformed"; exit 1; } [ -z "$MAX_BS" ] && { echo "ERROR: Could not parse batch size from engine filename: $ENGINE"; exit 1; } read IMGS_PER_SEC PEAK_GPU_STREAMS < <(python3 -c " import math imgs = float('$QPS_BS_MAX') * $MAX_BS print(round(imgs, 2), int(math.floor(imgs / 30))) ") # Parse DeepStream Run 1 and Run 2 FPS from logs written by pipeline-run # Fixed filename pattern: benchmarks/ds/ds_s{N}_run1.log and ds_s{N}_run2.log # Use glob to find them (N varies per model) then extract N from filename DS_LOG_RUN1=$(ls models/$MODEL_NAME/benchmarks/ds/ds_s*_run1.log 2>/dev/null | head -1) # Step 7's convergence loop can leave SEVERAL run2 logs (e.g. s17 -> s11 failed -> s9 passed). # Lexicographic `head -1` picks ds_s11 over ds_s9, reporting the NON-real-time run as the # validated result. Convergence steps downward, so the converged log is the newest — pick by mtime. # (`sort -V | tail -1` is NOT correct here: it re-selects the highest N, i.e. the failing run.) DS_LOG_RUN2=$(ls -t models/$MODEL_NAME/benchmarks/ds/ds_s*_run2.log 2>/dev/null | head -1) [ -z "$DS_LOG_RUN1" ] && { echo "ERROR: No DS Run 1 log found at benchmarks/ds/ds_s*_run1.log — run Steps 6-7 first (references/pipeline-run.md)"; exit 1; } [ -z "$DS_LOG_RUN2" ] && { echo "ERROR: No DS Run 2 log found at benchmarks/ds/ds_s*_run2.log — run Steps 6-7 first (references/pipeline-run.md)"; exit 1; } N_RUN1=$(basename "$DS_LOG_RUN1" | grep -oP 'ds_s\K[0-9]+(?=_run1)') N_RUN2=$(basename "$DS_LOG_RUN2" | grep -oP 'ds_s\K[0-9]+(?=_run2)') [[ "$N_RUN1" =~ ^[0-9]+$ ]] || { echo "ERROR: Could not parse stream count from $(basename "$DS_LOG_RUN1") — expected filename pattern ds_s<N>_run1.log"; exit 1; } [[ "$N_RUN2" =~ ^[0-9]+$ ]] || { echo "ERROR: Could not parse stream count from $(basename "$DS_LOG_RUN2") — expected filename pattern ds_s<N>_run2.log"; exit 1; } RT_STREAMS=$N_RUN2 # deepstream-app **PERF: format is `**PERF: fps_run0 (fps_avg0) fps_run1 (fps_avg1) ...` # Capture stream-0 instantaneous FPS (\K after `**PERF:`) — 1 value per line — so # tail -10 always covers exactly 10 measurement windows regardless of stream count. # Multiply by stream count for total throughput. FPS_RAW_RUN1=$(grep -oP '\*\*PERF:\s*\K[0-9.]+' "$DS_LOG_RUN1" | tail -10 | python3 -c " import sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)") FPS_RAW_RUN2=$(grep -oP '\*\*PERF:\s*\K[0-9.]+' "$DS_LOG_RUN2" | tail -10 | python3 -c " import sys; vals=[float(l) for l in sys.stdin if l.strip()]; print(round(sum(vals)/len(vals),2) if vals else 0)") TOTAL_FPS_RUN1=$(python3 -c "print(round(float('$FPS_RAW_RUN1') * $N_RUN1, 2))") TOTAL_FPS_RUN2=$(python3 -c "print(round(float('$FPS_RAW_RUN2') * $N_RUN2, 2))") echo "=== Report Variables ===" echo "MODEL_NAME=$MODEL_NAME MAX_BS=$MAX_BS" echo "BS=1: QPS=$QPS_BS1 GPU mean=${GPU_MEAN_BS1}ms" echo "BS=$MAX_BS: QPS=$QPS_BS_MAX imgs/s=$IMGS_PER_SEC PEAK_GPU_STREAMS=$PEAK_GPU_STREAMS" echo "DS Run 1: FPS/stream=$FPS_RAW_RUN1 streams=$N_RUN1 total=$TOTAL_FPS_RUN1 img/s" echo "DS Run 2: FPS/stream=$FPS_RAW_RUN2 streams=$N_RUN2 total=$TOTAL_FPS_RUN2 img/s RT_STREAMS=$RT_STREAMS" ``` Then immediately write `benchmark_data.json` before generating charts (so charts can load it if needed): ```bash mkdir -p models/$MODEL_NAME/reports python3 << 'EOF' import json, os def to_num(v, cast=float): """Return cast(v) or None if v is empty/invalid — prevents malformed JSON.""" try: return cast(v) if v and str(v).strip() else None except (ValueError, TypeError): return None data = { "model_name": os.environ.get("MODEL_NAME", ""), "engine": os.environ.get("ENGINE", ""), "max_bs": to_num(os.environ.get("MAX_BS"), int), "trtexec": { "bs1": { "qps": to_num(os.environ.get("QPS_BS1")), "gpu_mean_ms": to_num(os.environ.get("GPU_MEAN_BS1")) }, "bsmax": { "qps": to_num(os.environ.get("QPS_BS_MAX")), "gpu_mean_ms": to_num(os.environ.get("GPU_MEAN_BS_MAX")), "p99_ms": to_num(os.environ.get("GPU_P99_BS_MAX")), "imgs_per_sec": to_num(os.environ.get("IMGS_PER_SEC")) } }, "peak_gpu_streams": to_num(os.environ.get("PEAK_GPU_STREAMS"), int), "deepstream": { "run1": { "streams": to_num(os.environ.get("N_RUN1"), int), "total_fps": to_num(os.environ.get("TOTAL_FPS_RUN1")), "fps_per_stream": to_num(os.environ.get("FPS_RAW_RUN1")) }, "run2": { "streams": to_num(os.environ.get("N_RUN2"), int), "total_fps": to_num(os.environ.get("TOTAL_FPS_RUN2")), "fps_per_stream": to_num(os.environ.get("FPS_RAW_RUN2")) } } } out_path = os.path.join("models", os.environ.get("MODEL_NAME", "unknown"), "reports", "benchmark_data.json") with open(out_path, "w") as f: json.dump(data, f, indent=2) print("benchmark_data.json written") EOF ``` > `<< 'EOF'` (quoted) prevents bash expansion — Python reads all variables via `os.environ.get()`, applies `to_num()` for safe numeric conversion (returns `None` instead of producing malformed JSON when a variable is unset), then uses `json.dump` to guarantee valid output. ## 8c-1: Chart Generation (MANDATORY) All Python scripts in this step run inside the **shared venv** at `build/.venv_optimum` (which holds `matplotlib`, `numpy`, `markdown`, and `onnxruntime`). Activate it once before running any report scripts: ```bash source build/.venv_optimum/bin/activate ``` Generate exactly **5 charts** using `matplotlib` in `models/{model_name}/reports/charts/`. Use the script at `.claude/skills/deepstream-import-vision-model/scripts/report/generate-benchmark-charts.py` or generate manually. Chart names are fixed — do not rename them. | Filename | Content | Chart type | |----------|---------|------------| | `chart_trtexec_bs1_vs_bsmax.png` | Bar chart: QPS at BS=1 vs BS=MAX_BS (side by side) | Grouped bar | | `chart_trtexec_throughput.png` | GPU-only images/sec at MAX_BS, with PEAK_GPU_STREAMS annotation (dashed line at y=PEAK_GPU_STREAMS×30) | Single bar or line | | `chart_ds_streams_vs_fps.png` | Line chart: X=stream count (PEAK_GPU_STREAMS, RT_STREAMS), Y=FPS/stream. Red dashed line at 30fps threshold. | Line + markers | | `chart_trt_vs_ds.png` | Grouped bars: trtexec total imgs/s \| DS Run 1 total imgs/s \| DS Run 2 total imgs/s | Grouped bar | | `chart_efficiency.png` | DS efficiency %: 2 bars (Run 1 efficiency, Run 2 efficiency), dashed line at 100% | Bar | Do NOT generate H2D/D2H transfer overhead charts. Chart style requirements: - Figure size: `figsize=(10, 6)`, DPI: 150 - Title: two-line format via `two_line_title(model_name, subtitle)` — model name on line 1, chart description on line 2 (prevents long titles from clipping outside figure bounds) - Axis labels: 13px; Bar value labels: bold, 12-13px, positioned above bars - Grid: `axis='y', alpha=0.3`; `plt.tight_layout()` before save - Use `matplotlib.use('Agg')` (no display needed) ## 8c-1b: Markdown Report (MANDATORY) Generate `benchmark_report.md` before the HTML. This file must contain all 12 sections filled with actual values — no placeholders allowed. First, gather system info not already captured in pre-flight: ```bash GPU_INFO=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader | head -1) GPU_NAME=$(echo "$GPU_INFO" | cut -d, -f1 | xargs) GPU_VRAM=$(echo "$GPU_INFO" | cut -d, -f2 | xargs) DRIVER_VER=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -1 | xargs) CUDA_VER=$(nvcc --version 2>/dev/null | grep -oP 'release \K[0-9.]+' || echo "N/A") TRT_VER=$(trtexec 2>&1 | head -3 | grep -oP 'TensorRT v\K[0-9.]+' || echo "N/A") DS_VER=$(deepstream-app --version-all 2>/dev/null | grep -oP 'DeepStreamSDK \K[0-9.]+' || echo "N/A") ENGINE_SIZE_MB=$(du -m "$ENGINE" | cut -f1) IMGS_PER_SEC_BS1=$(python3 -c "print(round(float('$QPS_BS1') * 1, 2))") GPU_P99_BS1=$(grep -oP 'GPU Compute Time:.*percentile\(99%\) = \K[0-9.]+' "$TRTEXEC_LOG_BS1" | tail -1) GPU_P99_BS1=${GPU_P99_BS1:-"N/A"} # fallback if log too short to have P99 EFFICIENCY_RUN1=$(python3 -c "print(round(float('$TOTAL_FPS_RUN1') / float('$IMGS_PER_SEC') * 100, 1))") EFFICIENCY_RUN2=$(python3 -c "print(round(float('$TOTAL_FPS_RUN2') / float('$IMGS_PER_SEC') * 100, 1))") RT_LABEL_RUN1=$(python3 -c "print('YES' if float('$FPS_RAW_RUN1') >= 30 else 'NO')") RT_LABEL_RUN2=$(python3 -c "print('YES' if float('$FPS_RAW_RUN2') >= 30 else 'NO')") ``` Then write the markdown (use unquoted `<< MDEOF` so bash expands variables): ```bash cat > models/$MODEL_NAME/reports/benchmark_report.md << MDEOF # ${MODEL_NAME} Benchmark Report Generated: $(date '+%Y-%m-%d %H:%M:%S') --- ## 1. Model Configuration | Parameter | Value | |-----------|-------| | **Model Name** | ${MODEL_NAME} | | **Source** | (fill from Steps 1-3 log) | | **Architecture** | (fill from config.json model_type) | | **ONNX Source** | models/${MODEL_NAME}/model/ | | **Precision** | FP16 | | **Engine File** | $(basename $ENGINE) | | **Engine Profile** | min=1x3x640x640 opt=${MAX_BS}x3x640x640 max=${MAX_BS}x3x640x640 | | **Custom Parser** | libnvdsinfer_${MODEL_NAME}_parser.so | | **Cluster Mode** | (fill from nvinfer config) | ## 2. System Configuration | Parameter | Value | |-----------|-------| | **GPU** | ${GPU_NAME} | | **VRAM** | ${GPU_VRAM} | | **Driver** | ${DRIVER_VER} | | **CUDA** | ${CUDA_VER} | | **TensorRT** | ${TRT_VER} | | **DeepStream** | ${DS_VER} | ## 3. Preprocessing | Parameter | Value | |-----------|-------| | **net-scale-factor** | (fill from nvinfer config) | | **offsets** | (fill from nvinfer config) | | **Color Format** | (fill from nvinfer config) | | **Input Resolution** | 640×640 | ## 4. Engine Build Summary | Parameter | Value | |-----------|-------| | **Source Format** | ONNX | | **Engine File** | $(basename $ENGINE) | | **Engine Size** | ${ENGINE_SIZE_MB} MB | | **FP16** | Enabled | | **MAX Batch Size** | ${MAX_BS} | | **Workspace** | 32768 MiB | | **Timing Cache** | models/${MODEL_NAME}/benchmarks/engines/timing.cache | ## 5. trtexec Results | Metric | BS=1 | BS=${MAX_BS} | |--------|------|------| | **QPS (queries/s)** | ${QPS_BS1} | ${QPS_BS_MAX} | | **Images/s** | ${IMGS_PER_SEC_BS1} | ${IMGS_PER_SEC} | | **GPU Compute Mean (ms)** | ${GPU_MEAN_BS1} | ${GPU_MEAN_BS_MAX} | | **GPU Compute P99 (ms)** | ${GPU_P99_BS1} | ${GPU_P99_BS_MAX} | > Note: H2D/D2H latency excluded — trtexec run with \`--noDataTransfers\` to match DeepStream (GPU-to-GPU data flow, no host transfers).  ## 6. PEAK_GPU_STREAMS Derivation \`\`\` PEAK_GPU_STREAMS = floor(imgs_per_sec_at_MAX_BS / 30) = floor(${IMGS_PER_SEC} / 30) = ${PEAK_GPU_STREAMS} streams \`\`\`  ## 7. Single-Stream Validation | Parameter | Value | |-----------|-------| | **Video Source** | sample_720p.mp4 (1280×720) | | **KITTI Output Dir** | models/${MODEL_NAME}/samples/kitti_output/ | | **Total Frames** | (fill from kitti dump) | | **Frames with Detections** | (fill from kitti dump) | | **Detection Rate** | (fill — must be ≥ 90%) | | **Visual Capture Mode** | (fill: `nvv4l2h264enc MP4` OR `theoraenc OGV (NVENC unavailable)` OR `skipped (no encoder available)`) | | **Visual Capture Artifact** | (fill: `samples/${MODEL_NAME}_output.mp4` for NVENC path; `samples/${MODEL_NAME}_output.ogv` for theoraenc fallback; `N/A` if skipped) | | **Validation Result** | PASS | > **Encoder reporting rule (MANDATORY):** The Visual Capture Mode field MUST be exactly one of: > - `nvv4l2h264enc MP4` — NVENC succeeded; artifact is `.mp4` > - `theoraenc OGV (NVENC unavailable)` — if `DS_SINGLE_STREAM_MODE=theoraenc-fallback`; use `.ogv` path from `DS_SINGLE_STREAM_OUTPUT=` > - `skipped (no encoder available)` — if `DS_SINGLE_STREAM_MODE=skipped`; no artifact file > `x264enc` and `openh264enc` are prohibited and must never appear in this field. ## 8. DeepStream Benchmark Results ### DS Run 1 — Calibration at PEAK_GPU_STREAMS (${N_RUN1} streams) | Metric | Value | |--------|-------| | **Streams** | ${N_RUN1} | | **Batch Size** | ${N_RUN1} | | **FPS / Stream** | ${FPS_RAW_RUN1} | | **Total Images/s** | ${TOTAL_FPS_RUN1} | | **Real-Time (≥30 fps/stream)** | ${RT_LABEL_RUN1} | ### DS Run 2 — Validation at RT_STREAMS (${N_RUN2} streams) | Metric | Value | |--------|-------| | **Streams** | ${N_RUN2} | | **Batch Size** | ${N_RUN2} | | **FPS / Stream** | ${FPS_RAW_RUN2} | | **Total Images/s** | ${TOTAL_FPS_RUN2} | | **Real-Time (≥30 fps/stream)** | ${RT_LABEL_RUN2} |  ## 9. trtexec vs DeepStream Comparison | Metric | trtexec BS=${MAX_BS} | DS Run 1 (${N_RUN1} streams) | DS Run 2 (${N_RUN2} streams) | |--------|---------------------|------------------------------|------------------------------| | **Engine** | $(basename $ENGINE) | $(basename $ENGINE) | $(basename $ENGINE) | | **Batch / Streams** | BS=${MAX_BS} | ${N_RUN1} streams | ${N_RUN2} streams | | **Total imgs/s** | ${IMGS_PER_SEC} | ${TOTAL_FPS_RUN1} | ${TOTAL_FPS_RUN2} | | **FPS / stream** | $(python3 -c "print(round(float('$IMGS_PER_SEC')/${MAX_BS},1))") | ${FPS_RAW_RUN1} | ${FPS_RAW_RUN2} | | **Real-Time ≥30fps** | YES | ${RT_LABEL_RUN1} | ${RT_LABEL_RUN2} | | **DS Efficiency %** | — | ${EFFICIENCY_RUN1}% | ${EFFICIENCY_RUN2}% |  ## 10. Efficiency Analysis \`\`\` DS Efficiency = DS_total_imgs_per_sec / trtexec_imgs_per_sec × 100 Run 1: ${TOTAL_FPS_RUN1} / ${IMGS_PER_SEC} × 100 = ${EFFICIENCY_RUN1}% Run 2: ${TOTAL_FPS_RUN2} / ${IMGS_PER_SEC} × 100 = ${EFFICIENCY_RUN2}% \`\`\` Efficiency gap breakdown: NVDEC decode overhead (~5-10%), GStreamer mux/queue overhead (~5-10%), CPU scheduler jitter (~2-5%). Interpretation notes for the numbers above: - **Well-balanced pipeline**: GPU=99-100%, NVDEC=99-100%, CPU=30-40% with no single core pinned. The ~50% DS/trtexec gap at this utilization is physically irreducible — it's the cost of real decode + memory transfers that trtexec skips with \`--noDataTransfers\`. - **DS efficiency above 100% is expected for ViT / transformer models**: the TRT compiler backend (opt-level 4) often produces bimodal GPU latency with two alternating execution paths (e.g., 1.5ms and 4.0ms modes for OWL-ViT). trtexec reports high variance and a conservative median; DeepStream's pipelined scheduling smooths the bimodal pattern and can achieve 100-110% of the trtexec baseline. This is not a measurement error. - **1080p tends to saturate NVDEC** while GPU has headroom. The pipeline is pinned to 720p (\`sample_720p.mp4\`) specifically to keep benchmarks comparable across models.  ## 11. Pipeline Timing | Step | Description | Duration | |------|-------------|----------| | 1-3 | HF Model Acquire (download + inspect ONNX) | (fill from step timing) | | 4 | Engine build | (fill from step timing) | | 5 | trtexec BS=1 + BS=${MAX_BS} | (fill from step timing) | | 6 | Parser + config + visual validation + KITTI | (fill from step timing) | | 7 Run 1 | DS Calibration (${N_RUN1} streams) | (fill from step timing) | | 7 Run 2 | DS Validation (${N_RUN2} streams) | (fill from step timing) | | 8 | Report generation | (fill) | | **Total** | **End-to-end** | **(fill)** | ## 12. Reference Commands ### Engine Build \`\`\`bash trtexec --onnx=models/${MODEL_NAME}/model/${MODEL_FILENAME}.onnx \\ --saveEngine=models/${MODEL_NAME}/benchmarks/engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine \\ --minShapes=${INPUT_NAME}:1x3x${H}x${W} \\ --optShapes=${INPUT_NAME}:${MAX_BS}x3x${H}x${W} \\ --maxShapes=${INPUT_NAME}:${MAX_BS}x3x${H}x${W} \\ --fp16 --memPoolSize=workspace:32768M \\ --timingCacheFile=models/${MODEL_NAME}/benchmarks/engines/timing.cache \`\`\` ### trtexec Benchmark \`\`\`bash # BS=1 trtexec --loadEngine=$(basename $ENGINE) --shapes=${INPUT_NAME}:1x3x${H}x${W} \\ --noDataTransfers --warmUp=1000 --duration=10 # BS=${MAX_BS} trtexec --loadEngine=$(basename $ENGINE) --shapes=${INPUT_NAME}:${MAX_BS}x3x${H}x${W} \\ --noDataTransfers --warmUp=1000 --duration=10 \`\`\` ### DeepStream Single-Stream Validation \`\`\`bash # See models/${MODEL_NAME}/scripts/ for full gst-launch-1.0 command \`\`\` ### DeepStream Multi-Stream Benchmark \`\`\`bash # DS Run 1: ${N_RUN1} streams — see models/${MODEL_NAME}/scripts/ # DS Run 2: ${N_RUN2} streams — see models/${MODEL_NAME}/scripts/ \`\`\` ### Custom Parser Build \`\`\`bash cd models/${MODEL_NAME}/parser && make DEEPSTREAM_DIR=/opt/nvidia/deepstream/deepstream CUDA_VER=13.2 \`\`\` MDEOF echo "benchmark_report.md written: $(wc -l < models/$MODEL_NAME/reports/benchmark_report.md) lines" ``` > **Note on "fill" fields**: Fields marked `(fill from ...)` must be replaced with actual values from the step logs before finalizing. Search the step output logs for the exact values and substitute them. Do not leave any `(fill ...)` placeholder in the final report. ## 8c-2 + 8c-3: HTML + PDF Report (MANDATORY — ONE COMMAND) Before generating HTML+PDF, verify all 5 charts exist: ```bash CHART_DIR="models/$MODEL_NAME/reports/charts" MISSING_CHARTS=0 for CHART in chart_trtexec_bs1_vs_bsmax.png chart_trtexec_throughput.png \ chart_ds_streams_vs_fps.png chart_trt_vs_ds.png chart_efficiency.png; do [ ! -f "$CHART_DIR/$CHART" ] && { echo "ERROR: Missing $CHART_DIR/$CHART"; MISSING_CHARTS=$((MISSING_CHARTS+1)); } done [ "$MISSING_CHARTS" -gt 0 ] && { echo "ERROR: $MISSING_CHARTS chart(s) missing — re-run 8c-1"; exit 1; } echo "All 5 charts verified OK" ``` Then run the canonical pipeline script — this generates BOTH the HTML and PDF correctly: ```bash # setup.sh apt-installs wkhtmltopdf INSIDE the container, so it does NOT survive the ephemeral # `--rm` bootstrap container. Every later `docker run --rm` starts without it and the PDF step # silently degrades to HTML-only. Ensure it is present in THIS invocation before rendering. command -v wkhtmltopdf >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq wkhtmltopdf; } python3 .claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py \ models/$MODEL_NAME/reports/benchmark_report.md \ .claude/skills/deepstream-import-vision-model/scripts/report/report-style.css \ models/$MODEL_NAME/reports/ \ $MODEL_NAME ``` This script uses `report_style.css` (navy `#283593` headers, `#e8eaf6` rows, `#263238` code blocks), embeds charts as base64 data URIs, calls `wkhtmltopdf` internally, and outputs `benchmark_report.html` + `benchmark_report_{model_name}.pdf`. > **NAMING RULES:** > - HTML: always `benchmark_report.html` (no model name suffix) > - PDF: always `benchmark_report_{model_name}.pdf` (model name postfix required) Verify PDF size is >500 KB (confirms charts embedded). Run all python commands with the shared venv active (`source build/.venv_optimum/bin/activate`); `markdown` and `matplotlib` are already installed there. ## 8c-4: Final Report Checklist and Timing After generating markdown, HTML, and PDF, record step timing: ```bash STEP8_END=$(date +%s.%N) STEP8_DURATION=$(python3 -c "print(round($STEP8_END - $STEP8_START, 2))") # bc is not in the container; python3 always is echo "[Step 8] Report generation completed in ${STEP8_DURATION}s" ``` Before marking the report as complete, verify ALL of these exist: - [ ] `reports/benchmark_report.md` — markdown source (12 sections) - [ ] `reports/benchmark_report.html` — styled HTML (charts/ alongside) - [ ] `reports/benchmark_report_{model_name}.pdf` — PDF >500 KB (confirms charts embedded) - [ ] `reports/benchmark_data.json` — raw benchmark numbers - [ ] `reports/charts/` — all 5 PNGs: `chart_trtexec_bs1_vs_bsmax.png`, `chart_trtexec_throughput.png`, `chart_ds_streams_vs_fps.png`, `chart_trt_vs_ds.png`, `chart_efficiency.png` - **Charts**: fixed filenames above — never rename or add model name suffix to charts -
windows.md 3.3 KB
<!-- Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"). --> # Running on Windows (and cross-platform) This skill runs on **Windows with Docker Desktop** with **no host packages**: every compute step runs **inside the DeepStream Linux container** via `docker run`. The host only needs `docker` + the NVIDIA driver. The single exception is the host-side install (copying the skill into `<project>\.claude\skills\`), which cannot run in a container — so the skill ships exactly one PowerShell script, **`install.ps1`**, the twin of `install.sh`. There are no `.ps1` duplicates of the compute scripts. ## Why this works The ONNX export, TensorRT engine build, custom nvinfer parser compile, DeepStream run, and PDF report all execute **inside** `nvcr.io/nvidia/deepstream:9.1-triton-multiarch` (the venv `build/.venv_optimum` + `wkhtmltopdf` are installed into that container by `setup.sh`). The container is the portability layer. The only per-shell difference is the `docker run` **bind-mount token**. ## Prerequisites (Windows) 1. **Docker Desktop** with the **WSL2 backend enabled** (Settings → General → *Use the WSL 2 based engine*) — required for GPU (`--gpus all` works only through the WSL2 backend). 2. A recent **NVIDIA driver** with WSL/CUDA support. No CUDA toolkit / TensorRT / DeepStream needed on the host — the container ships them. 3. Docker Desktop → Settings → Resources → **File Sharing**: share the drive holding your working dir. 4. `docker pull nvcr.io/nvidia/deepstream:9.1-triton-multiarch`. ## The one thing that differs per shell: the mount token Claude Code fills this in based on the host OS: | Shell | working-dir mount | |-------|-------------------| | **PowerShell** | `-v "${PWD}:/work"` | | **cmd** | `-v "%cd%:/work"` | | **WSL2 / Linux bash** | `-v "$PWD":/work` | ## Bootstrap + preflight (PowerShell example) ```powershell # one-time bootstrap: venv + torch/onnx/onnxruntime + wkhtmltopdf, all in-container docker run --rm -it --gpus all --shm-size=16g -v "${PWD}:/work" -w /work ` --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch ` .claude/skills/deepstream-import-vision-model/setup.sh # preflight — GPU + venv + trtexec (container-mode auto-detects /.dockerenv) docker run --rm --gpus all -v "${PWD}:/work" -w /work ` --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch ` .claude/skills/deepstream-import-vision-model/scripts/preflight.sh ``` Every subsequent phase runs the same way — via `docker run … -lc '<commands>'` or the `.claude/skills/deepstream-import-vision-model/scripts/dsrun.sh` wrapper. ## Notes - The skill ships a `.gitattributes` forcing **LF** on all scripts, so a Windows checkout won't CRLF-corrupt them (CRLF breaks bash-in-container). - `--shm-size=16g` works on the WSL2 backend. - **Install:** on native Windows run the bundled **`install.ps1`** — the twin of `install.sh`, same sequence and flags (`-Target`=`--target`, `-NoCursor`=`--no-cursor`, `-DryRun`=`--dry-run`): `.\install.ps1 -Target C:\path\to\project` (copies the skill into `<project>\.claude\skills\`). On Linux/WSL2/Git Bash use `bash install.sh --target <project>`. - Prefer a **WSL2 Ubuntu terminal** for the exact Linux experience — inside WSL2 everything runs unchanged.
-
-
scripts
-
deepstream
-
benchmark-ds.sh 3.6 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. set -euo pipefail ################################################################################ # DeepStream benchmark using gst-launch-1.0 # Thumb rule: batch_size == num_streams (always equal). # Measures total throughput by timing full video processing with fakesink. # # Usage: ./benchmark-ds.sh <config_file> <num_streams> [input_video] # Example: ./benchmark-ds.sh config_infer_primary_b21.txt 21 video.mp4 # # batch_size in the nvinfer config must match num_streams. ################################################################################ CONFIG="${1:-}" NUM_STREAMS="${2:-}" VIDEO="${3:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}" MUXER_W=1280 MUXER_H=720 NS_PER_SEC=$(( 1000 * 1000 * 1000 )) if [ -z "$CONFIG" ] || [ -z "$NUM_STREAMS" ]; then echo "Usage: $0 <config_file> <num_streams> [input_video]" exit 1 fi # Detect video FPS via mediainfo; fall back to 30 for the standard sample VIDEO_FPS=$(mediainfo --Inform="Video;%FrameRate%" "${VIDEO}" 2>/dev/null | awk '{printf "%.0f", $1+0}') VIDEO_FPS="${VIDEO_FPS:-30}" # Detect actual frame count; fall back to 1440 if mediainfo unavailable or fails if [ -n "$3" ]; then FRAMES_PER_STREAM=$(mediainfo --Inform="Video;%FrameCount%" "${VIDEO}" 2>/dev/null) if ! echo "$FRAMES_PER_STREAM" | grep -qE '^[0-9]+$' || [ "$FRAMES_PER_STREAM" -eq 0 ]; then echo "Warning: mediainfo failed, falling back to 1440 frames" >&2 FRAMES_PER_STREAM=1440 fi else # Default sample_720p.mp4 is ~1440 frames at 30fps FRAMES_PER_STREAM=1440 fi TOTAL_FRAMES=$((FRAMES_PER_STREAM * NUM_STREAMS)) echo "=== DeepStream Benchmark ===" echo "Config: $CONFIG" echo "Streams: $NUM_STREAMS" echo "Frames/stream: $FRAMES_PER_STREAM" echo "Total frames: $TOTAL_FRAMES" echo "" # Build source elements SOURCES="" for i in $(seq 0 $((NUM_STREAMS - 1))); do SOURCES+="filesrc location=${VIDEO} ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! mux.sink_${i} " done PIPELINE="${SOURCES} nvstreammux name=mux batch-size=${NUM_STREAMS} width=${MUXER_W} height=${MUXER_H} batched-push-timeout=-1 ! \ queue ! nvinfer config-file-path=${CONFIG} ! queue ! fakesink sync=0" echo "Starting pipeline..." START_TIME=$(date +%s%N) GST_DEBUG=0 gst-launch-1.0 -e ${PIPELINE} 2>&1 | grep -v "^$" || true END_TIME=$(date +%s%N) ELAPSED_NS=$((END_TIME - START_TIME)) # awk instead of bc (bc is not installed in the DeepStream container; awk always is) ELAPSED_SEC=$(awk "BEGIN{printf \"%.2f\", $ELAPSED_NS / $NS_PER_SEC}") FPS=$(awk "BEGIN{printf \"%.1f\", $TOTAL_FRAMES / $ELAPSED_SEC}") REALTIME=$(awk "BEGIN{printf \"%.2f\", $FPS / (${NUM_STREAMS} * ${VIDEO_FPS})}") echo "" echo "=== Results ===" echo "Wall time: ${ELAPSED_SEC}s" echo "Total frames: ${TOTAL_FRAMES}" echo "Throughput: ${FPS} img/s" echo "Per-stream: $(awk "BEGIN{printf \"%.1f\", $FPS / $NUM_STREAMS}") fps" echo "Real-time factor: ${REALTIME}x (${NUM_STREAMS} streams @ ${VIDEO_FPS}fps)" echo "===============" -
ds-kitti-dump.sh 4.5 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. ################################################################################ # Step 6: KITTI dump using deepstream-app (built-in KITTI support) # Generates a temporary deepstream-app config, runs for N frames, dumps KITTI. # # Usage: ./ds-kitti-dump.sh <nvinfer_config> <kitti_output_dir> [num_frames] [input_video] # Example: ./ds-kitti-dump.sh config_infer_primary_yolox.txt kitti_output 100 ################################################################################ set -euo pipefail NVINFER_CONFIG="$1" KITTI_DIR="$2" NUM_FRAMES="${3:-100}" VIDEO="${4:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}" if [ -z "$NVINFER_CONFIG" ] || [ -z "$KITTI_DIR" ]; then echo "Usage: $0 <nvinfer_config> <kitti_output_dir> [num_frames] [input_video]" exit 1 fi # Validate inputs before resolving paths [ -f "$NVINFER_CONFIG" ] || { echo "ERROR: nvinfer config not found: $NVINFER_CONFIG"; exit 1; } [ -f "$VIDEO" ] || { echo "ERROR: video file not found: $VIDEO"; exit 1; } # Resolve to absolute paths NVINFER_CONFIG="$(realpath "$NVINFER_CONFIG")" KITTI_DIR="$(realpath -m "$KITTI_DIR")" VIDEO="$(realpath "$VIDEO")" mkdir -p "${KITTI_DIR}" echo "=== DeepStream KITTI Dump ===" echo "nvinfer config: $NVINFER_CONFIG" echo "KITTI dir: $KITTI_DIR" echo "Max frames: $NUM_FRAMES" echo "Input video: $VIDEO" echo "" # Generate temporary deepstream-app config trap 'rm -f "${TMPCONFIG:-}"' EXIT TMPCONFIG=$(mktemp /tmp/ds_kitti_XXXXXX.txt) cat > "$TMPCONFIG" << EOF [application] enable-perf-measurement=0 gie-kitti-output-dir=${KITTI_DIR} [tiled-display] enable=0 [source0] enable=1 type=3 uri=file://${VIDEO} num-sources=1 gpu-id=0 [sink0] enable=1 type=1 #1=FakeSink sync=0 [osd] enable=0 [streammux] live-source=0 batch-size=1 batched-push-timeout=-1 width=1280 height=720 [primary-gie] enable=1 batch-size=1 gie-unique-id=1 config-file=${NVINFER_CONFIG} [tests] file-loop=0 EOF echo "Temp config: $TMPCONFIG" echo "Running deepstream-app..." # Run deepstream-app (it will process entire video). # Temporarily disable pipefail so head -30 closing the pipe early (SIGPIPE to grep) # doesn't trigger set -e before we can capture deepstream-app's exit code. set +o pipefail timeout 120 deepstream-app -c "$TMPCONFIG" 2>&1 | grep -v "^$" | head -30 DS_EXIT_CODE=${PIPESTATUS[0]} set -o pipefail if [ $DS_EXIT_CODE -eq 124 ]; then echo "Warning: deepstream-app timed out after 120 seconds" elif [ $DS_EXIT_CODE -ne 0 ]; then echo "Error: deepstream-app failed with exit code $DS_EXIT_CODE" exit 1 fi # Count KITTI files generated TOTAL_FILES=$(ls -1 "${KITTI_DIR}"/*.txt 2>/dev/null | wc -l) echo "" echo "Total KITTI files generated: ${TOTAL_FILES}" # Keep only first N frames, remove the rest if [ "$TOTAL_FILES" -gt "$NUM_FRAMES" ]; then # Guard against misconfigured KITTI_DIR blowing away something else [ -n "$KITTI_DIR" ] && [ -d "$KITTI_DIR" ] && [ "$KITTI_DIR" != "/" ] \ || { echo "ERROR: invalid KITTI_DIR for cleanup: $KITTI_DIR"; exit 1; } TO_REMOVE=$((TOTAL_FILES - NUM_FRAMES)) echo "Trimming to first ${NUM_FRAMES} frames (removing ${TO_REMOVE})..." # NUL-delimited read so filenames with spaces/newlines are handled safely. KITTI_FILES=() while IFS= read -r -d '' f; do KITTI_FILES+=("$f") done < <(find "$KITTI_DIR" -maxdepth 1 -type f -name '*.txt' -print0 | sort -z) for ((i = NUM_FRAMES; i < ${#KITTI_FILES[@]}; i++)); do rm -f -- "${KITTI_FILES[i]}" done TOTAL_FILES=$(find "$KITTI_DIR" -maxdepth 1 -type f -name '*.txt' 2>/dev/null | wc -l) echo "Kept ${TOTAL_FILES} KITTI files" fi # Show sample KITTI output echo "" echo "=== Sample KITTI Output (first 3 files) ===" for f in $(ls -1 "${KITTI_DIR}"/*.txt 2>/dev/null | sort | head -3); do echo "--- $(basename $f) ---" cat "$f" done echo "" echo "=== KITTI Dump Complete ===" -
ds-perf-run.sh 4.8 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. ################################################################################ # Step 7c: DeepStream perf-measurement run via deepstream-app. # # Replaces the older `gst-launch-1.0 ... ! fpsdisplaysink ...` benchmark, which # pulled in `gstreamer1.0-plugins-bad`. `deepstream-app` is part of the NVIDIA # DeepStream SDK and emits `**PERF: fps_run0 (fps_avg0) fps_run1 (fps_avg1) ...` # lines (one pair per active source) that the report-generation phase parses. # # Usage: ./ds-perf-run.sh <nvinfer_config> <num_streams> <log_path> [input_video] # Example: # ./ds-perf-run.sh config_infer_ds_yolox.txt 32 \ # models/yolox/benchmarks/ds/ds_s32_run1.log \ # /opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4 # # Notes: # - `[primary-gie] batch-size` and `[streammux] batch-size` are both set to N # (matches the skill-wide rule batch_size == num_streams). # - `num-sources=N` fans the single input video out to N pipeline sources; # deepstream-app handles the file-loop / EOS bookkeeping. # - The nvinfer config must already point at the engine, parser, and labels. # This script does NOT mutate the nvinfer config. ################################################################################ set -euo pipefail NVINFER_CONFIG="${1:-}" NUM_STREAMS="${2:-}" LOG_PATH="${3:-}" VIDEO="${4:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}" if [ -z "$NVINFER_CONFIG" ] || [ -z "$NUM_STREAMS" ] || [ -z "$LOG_PATH" ]; then echo "Usage: $0 <nvinfer_config> <num_streams> <log_path> [input_video]" exit 1 fi [ -f "$NVINFER_CONFIG" ] || { echo "ERROR: nvinfer config not found: $NVINFER_CONFIG"; exit 1; } [ -f "$VIDEO" ] || { echo "ERROR: video file not found: $VIDEO"; exit 1; } command -v deepstream-app >/dev/null 2>&1 || { echo "ERROR: deepstream-app not on PATH"; exit 1; } NVINFER_CONFIG="$(realpath "$NVINFER_CONFIG")" VIDEO="$(realpath "$VIDEO")" LOG_PATH="$(realpath -m "$LOG_PATH")" mkdir -p "$(dirname "$LOG_PATH")" N="$NUM_STREAMS" MUXER_W=1280 MUXER_H=720 echo "=== DeepStream Perf Run ===" echo "nvinfer config: $NVINFER_CONFIG" echo "Streams (=N): $N" echo "Input video: $VIDEO" echo "Log path: $LOG_PATH" echo "" trap 'rm -f "${TMPCONFIG:-}"' EXIT TMPCONFIG=$(mktemp /tmp/ds_perf_XXXXXX.txt) cat > "$TMPCONFIG" <<EOF [application] enable-perf-measurement=1 perf-measurement-interval-sec=2 [tiled-display] enable=0 [source0] enable=1 type=3 uri=file://${VIDEO} num-sources=${N} gpu-id=0 [sink0] enable=1 type=1 sync=0 [osd] enable=0 [streammux] live-source=0 batch-size=${N} batched-push-timeout=-1 width=${MUXER_W} height=${MUXER_H} [primary-gie] enable=1 batch-size=${N} gie-unique-id=1 config-file=${NVINFER_CONFIG} [tests] file-loop=1 EOF echo "Temp config: $TMPCONFIG" echo "Running deepstream-app..." set +o pipefail # file-loop=1 has no built-in stop condition; timeout(1) kills deepstream-app # after 60 s and returns exit 124 — treated as success below. timeout 60s deepstream-app -c "$TMPCONFIG" 2>&1 | tee "$LOG_PATH" DS_EXIT_CODE=${PIPESTATUS[0]} set -o pipefail # exit 124 = timeout fired as expected (file-loop=1, 60 s cap) if [ $DS_EXIT_CODE -ne 0 ] && [ $DS_EXIT_CODE -ne 124 ]; then echo "ERROR: deepstream-app exited with code $DS_EXIT_CODE — see $LOG_PATH" >&2 exit "$DS_EXIT_CODE" fi # Average stream-0 instantaneous FPS across the last 10 **PERF: lines. # Using stream 0 (the \K capture after `**PERF:`) gives exactly 1 value per # measurement window so tail -10 always covers 10 windows regardless of N. # Multiply by N for total throughput. PERF_FPS=$(grep -oP '\*\*PERF:\s*\K[0-9.]+' "$LOG_PATH" | tail -10 | python3 -c " import sys vals = [float(line) for line in sys.stdin if line.strip()] print(round(sum(vals)/len(vals), 2) if vals else 0) ") if [ -z "$PERF_FPS" ] || [ "$PERF_FPS" = "0" ]; then echo "ERROR: no **PERF: lines parsed from $LOG_PATH" >&2 exit 1 fi TOTAL_FPS=$(python3 -c "print(round(float('$PERF_FPS') * $N, 2))") echo "" echo "=== Perf Summary ===" echo "Streams: $N" echo "FPS/stream: $PERF_FPS" echo "Total imgs/sec: $TOTAL_FPS" echo "Log: $LOG_PATH" echo "====================" -
ds-single-stream.sh 4.9 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. ################################################################################ # Step 6: Single-stream DeepStream pipeline -- saves output video with OSD boxes. # # Usage: ./ds-single-stream.sh <config_file> <output_video> [input_video] # Example: ./ds-single-stream.sh config_infer_primary_yolox.txt yolox_output.mp4 # # Encoder policy (MANDATORY): # - Primary path uses nvv4l2h264enc (NVENC) with .mp4 container. nvdsosd # overlays are reliably preserved only with NVENC on the NVMM memory path. # - x264enc and openh264enc are PROHIBITED and must never be used. # - On NVENC-init failure, the script checks theoraenc + oggmux availability # (LGPL elements; both ship in gst-plugins-base): # * Available → falls back to theoraenc+oggmux → saves <output>.ogv # nvvideoconvert ! "video/x-raw, format=I420" ! theoraenc quality=48 ! oggmux # Emits DS_SINGLE_STREAM_MODE=theoraenc-fallback and DS_SINGLE_STREAM_OUTPUT=<path> # * Unavailable → skips video creation, emits DS_SINGLE_STREAM_MODE=skipped, exit 0 # The benchmark report must surface which encoder mode was used. ################################################################################ set -o pipefail CONFIG="$1" OUTPUT="$2" VIDEO="${3:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}" MUXER_W=1280 MUXER_H=720 if [ -z "$CONFIG" ] || [ -z "$OUTPUT" ]; then echo "Usage: $0 <config_file> <output_video> [input_video]" exit 1 fi OUTPUT_DIR="$(dirname "$OUTPUT")" LOG_FILE="$(mktemp -t ds-single-stream-XXXXXX.log)" trap 'rm -f "$LOG_FILE"' EXIT mkdir -p "$OUTPUT_DIR" echo "=== DeepStream Single-Stream Test ===" echo "Config: $CONFIG" echo "Input: $VIDEO" echo "Output: $OUTPUT (primary: nvv4l2h264enc)" echo "" gst-launch-1.0 \ filesrc location="${VIDEO}" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! mux.sink_0 \ nvstreammux name=mux batch-size=1 width=${MUXER_W} height=${MUXER_H} batched-push-timeout=-1 ! \ nvinfer config-file-path="${CONFIG}" ! \ nvvideoconvert ! nvdsosd ! nvvideoconvert ! \ "video/x-raw(memory:NVMM), format=NV12" ! nvv4l2h264enc ! h264parse ! mp4mux ! \ filesink location="${OUTPUT}" sync=0 \ 2>&1 | tee "$LOG_FILE" STATUS=${PIPESTATUS[0]} if [ $STATUS -eq 0 ] && [ -s "$OUTPUT" ]; then echo "" echo "Output saved to: ${OUTPUT}" echo "DS_SINGLE_STREAM_MODE=nvenc-primary" echo "DS_SINGLE_STREAM_OUTPUT=${OUTPUT}" exit 0 fi # Detect NVENC-init failure -- the only condition under which we use the theoraenc fallback. # x264enc and openh264enc are prohibited. Any other failure surfaces as a hard error. if grep -qE "v4l2-nvenc.*failed during initialization|Could not open device.*v4l2-nvenc|nvv4l2h264enc.*not-negotiated" "$LOG_FILE"; then echo "" echo "WARNING: nvv4l2h264enc (NVENC) is unavailable on this GPU." >&2 if ! gst-inspect-1.0 theoraenc > /dev/null 2>&1 || ! gst-inspect-1.0 oggmux > /dev/null 2>&1; then echo "WARNING: theoraenc/oggmux not available. Skipping video creation." >&2 echo "DS_SINGLE_STREAM_MODE=skipped" exit 0 fi echo " Falling back to theoraenc+oggmux (OGV output)." >&2 echo "" OGV_OUTPUT="$(echo "${OUTPUT}" | sed -E 's/\.[Mm][Pp]4$//').ogv" rm -f "$OUTPUT" "$OGV_OUTPUT" gst-launch-1.0 \ filesrc location="${VIDEO}" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! mux.sink_0 \ nvstreammux name=mux batch-size=1 width=${MUXER_W} height=${MUXER_H} batched-push-timeout=-1 ! \ nvinfer config-file-path="${CONFIG}" ! \ nvvideoconvert ! nvdsosd ! nvvideoconvert ! \ "video/x-raw, format=I420" ! theoraenc quality=48 ! oggmux ! \ filesink location="${OGV_OUTPUT}" sync=0 \ 2>&1 THEORA_STATUS=$? if [ $THEORA_STATUS -eq 0 ] && [ -s "$OGV_OUTPUT" ]; then echo "" echo "theoraenc fallback succeeded. Output saved to: ${OGV_OUTPUT}" echo "DS_SINGLE_STREAM_MODE=theoraenc-fallback" echo "DS_SINGLE_STREAM_OUTPUT=${OGV_OUTPUT}" exit 0 fi echo "ERROR: theoraenc fallback pipeline failed (exit ${THEORA_STATUS})." >&2 exit ${THEORA_STATUS:-1} fi echo "Pipeline failed with exit code $STATUS (not an NVENC-init failure)." >&2 exit $STATUS -
ds-sweep.sh 12.3 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. ################################################################################ # DeepStream BS_OPT sweep — smart 2-phase approach. # # Phase 1: trtexec probe at BS=1,4,8 (~30s total, fast) # - Fits power-law curve: QPS = a × BS^(-alpha) # - Predicts BS where trtexec QPS = FPS_THRESHOLD / DS_EFFICIENCY # - This accounts for DeepStream pipeline overhead vs raw trtexec # # Phase 2: DeepStream confirmation (1-2 runs) # - Runs DS at BS_pred and BS_pred-step if needed # - Picks highest BS where DS fps/stream >= FPS_THRESHOLD # - Uses dynamic engine (no per-BS engine builds during sweep) # # Thumb rules: # - batch_size == num_streams (always equal) # - Dynamic engine: min=1, opt=10, max=max(BATCH_SIZES_PROBE) e.g. 8 # Extended at build time to max=BS_pred+margin once predicted # - BS_OPT drives production engine build (static, timing cache reuse) # # Usage: # ./ds-sweep.sh <dynamic_engine> <onnx_path> <config_template> \ # <parser_so> <labels> <engines_dir> <configs_dir> [video] ################################################################################ set -euo pipefail DYNAMIC_ENGINE="$1" ONNX_PATH="$2" CONFIG_TEMPLATE="$3" PARSER_SO="$4" LABELS="$5" ENGINES_DIR="$6" CONFIGS_DIR="$7" VIDEO="${8:-/opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4}" # Derive INPUT_NAME, H, W from the ONNX model — mirrors how engine-build.md does it. # Env var overrides let callers handle models with dynamic spatial dims (e.g. H=800 W=800 ./ds-sweep.sh ...). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INSPECT_SCRIPT="$(realpath "${SCRIPT_DIR}/../../model/inspect-onnx.py")" if [ -z "${INPUT_NAME:-}" ] || [ -z "${H:-}" ] || [ -z "${W:-}" ]; then INSPECT_OUT=$(python3 "${INSPECT_SCRIPT}" "${ONNX_PATH}") INPUT_NAME="${INPUT_NAME:-$(echo "${INSPECT_OUT}" | grep -oP 'input_name:\s*\K\S+')}" H="${H:-$(echo "${INSPECT_OUT}" | grep -oP 'height:\s*\K[0-9]+')}" W="${W:-$(echo "${INSPECT_OUT}" | grep -oP 'width:\s*\K[0-9]+')}" fi [ -z "${INPUT_NAME}" ] && { echo "ERROR: could not parse INPUT_NAME from ONNX — set INPUT_NAME env var"; exit 1; } [ -z "${H}" ] && { echo "ERROR: H not detected (dynamic spatial dims) — set H env var, e.g. H=800"; exit 1; } [ -z "${W}" ] && { echo "ERROR: W not detected (dynamic spatial dims) — set W env var, e.g. W=800"; exit 1; } # DS_ERR_LOG: destination for GStreamer/DeepStream stderr output. # Override via environment variable to redirect elsewhere (e.g. a file path or /dev/stderr). # Defaults to a log file alongside the sweep engine logs so errors are preserved for diagnosis. DS_ERR_LOG="${DS_ERR_LOG:-${ENGINES_DIR}/ds_sweep_gst_errors.log}" mkdir -p "$(dirname "${DS_ERR_LOG}")" # Truncate/create the log at the start of the sweep so it reflects the current run only. : > "${DS_ERR_LOG}" echo "[ds-sweep] GStreamer stderr → ${DS_ERR_LOG}" # TIMING_CACHE="${ENGINES_DIR}/timing.cache" # used by the engine-build phase, not sweep NS_PER_SEC=$(( 1000 * 1000 * 1000 )) # nanoseconds per second (date +%s%N divisor) FPS_THRESHOLD=30 # target fps/stream in DeepStream DS_EFFICIENCY=0.65 # DS is ~65% of trtexec throughput (GStreamer pipeline overhead # includes muxer, memory mgmt, custom parser, metadata — measured) TRT_QPS_TARGET=$(awk "BEGIN{printf \"%.4f\", ${FPS_THRESHOLD} / ${DS_EFFICIENCY}}") # ~46.2 QPS (awk: bc absent in container) PROBE_SIZES=(1 4 8) # fast trtexec probe batch sizes PROBE_DURATION=10 # seconds per trtexec probe run # NEVER use filesrc num-buffers as a frame count — num-buffers counts file byte blocks (4096B), # not video frames. Leave num-buffers unset so filesrc reads to natural EOS. # Detect actual frame count and FPS via mediainfo — consistent with benchmark-ds.sh. VIDEO_FPS=$(mediainfo --Inform="Video;%FrameRate%" "${VIDEO}" 2>/dev/null | awk '{printf "%.0f", $1+0}') VIDEO_FPS="${VIDEO_FPS:-30}" ACTUAL_FRAMES_PER_STREAM=$(mediainfo --Inform="Video;%FrameCount%" "${VIDEO}" 2>/dev/null) if ! echo "${ACTUAL_FRAMES_PER_STREAM}" | grep -qE '^[0-9]+$' || [ "${ACTUAL_FRAMES_PER_STREAM:-0}" -eq 0 ]; then ACTUAL_FRAMES_PER_STREAM=1440 # fallback for sample_720p.mp4: ~48s × 30fps fi echo " Video frames/stream: ${ACTUAL_FRAMES_PER_STREAM} (${VIDEO_FPS}fps detected)" MUXER_W=1280 MUXER_H=720 mkdir -p "${CONFIGS_DIR}" echo "======================================================" echo "DS BS_OPT Sweep — 2-Phase Smart Search" echo " FPS threshold : ${FPS_THRESHOLD} fps/stream" echo " DS efficiency : ${DS_EFFICIENCY} (trtexec QPS target: ${TRT_QPS_TARGET})" echo " Probe sizes : ${PROBE_SIZES[*]}" echo " Input tensor : ${INPUT_NAME} (${H}x${W})" echo "======================================================" # ── PHASE 1: trtexec probe at BS=1,4,8 ────────────────── echo "" echo "PHASE 1: trtexec probe (BS=${PROBE_SIZES[*]})" declare -a PROBE_BS_ARR PROBE_QPS_ARR for BS in "${PROBE_SIZES[@]}"; do echo " trtexec BS=${BS}..." LOG="${ENGINES_DIR}/probe_bs${BS}.log" trtexec \ --loadEngine="${DYNAMIC_ENGINE}" \ --fp16 \ --shapes=${INPUT_NAME}:${BS}x3x${H}x${W} \ --duration=${PROBE_DURATION} \ --warmUp=2000 \ > "${LOG}" 2>&1 QPS=$(grep "Throughput:" "${LOG}" | grep -oP 'Throughput: \K[0-9.]+' | head -1) echo " BS=${BS}: ${QPS} QPS" PROBE_BS_ARR+=("${BS}") PROBE_QPS_ARR+=("${QPS}") done # ── Power-law fit: QPS = a × BS^(-alpha) ──────────────── # Use BS=4 and BS=8 points to fit alpha (most stable region) # alpha = log(QPS4/QPS8) / log(8/4) QPS4="${PROBE_QPS_ARR[1]}" QPS8="${PROBE_QPS_ARR[2]}" ALPHA=$(python3 -c " import math qps4, qps8 = float('${QPS4}'), float('${QPS8}') alpha = math.log(qps4 / qps8) / math.log(8.0 / 4.0) print(f'{alpha:.4f}') ") A_COEFF=$(python3 -c " import math qps8, alpha = float('${QPS8}'), float('${ALPHA}') a = qps8 * (8.0 ** alpha) print(f'{a:.4f}') ") echo "" echo " Curve fit: QPS = ${A_COEFF} × BS^(-${ALPHA})" # Solve for BS where QPS = TRT_QPS_TARGET # BS_pred = (a / QPS_target)^(1/alpha) # Guard: if alpha ~ 0 (flat curve — memory-bandwidth-bound or very small model), # 1/alpha diverges. Use the cap directly and let Phase 2 DS runs confirm. BS_PRED=$(python3 -c " import math a, alpha = float('${A_COEFF}'), float('${ALPHA}') target = float('${TRT_QPS_TARGET}') if abs(alpha) < 1e-3: bs_pred = 128 else: bs_pred = (a / target) ** (1.0 / alpha) print(int(bs_pred)) ") echo " Predicted BS_pred = ${BS_PRED} (trtexec QPS ≈ ${TRT_QPS_TARGET} at this batch)" echo "" # Clamp BS_pred to reasonable range [8, 128] BS_PRED=$(python3 -c "print(max(8, min(128, int('${BS_PRED}'))))") # ── PHASE 2: DeepStream confirmation ──────────────────── echo "PHASE 2: DeepStream confirmation around BS_pred=${BS_PRED}" # Test BS_pred and BS_pred - small step if first fails # Round BS_pred to nearest sensible value BS_STEP=$(python3 -c " bs = int('${BS_PRED}') # step = ~10% of BS_pred, minimum 1 step = max(1, round(bs * 0.1)) print(step) ") CANDIDATES=("${BS_PRED}") BS_LOWER=$(( BS_PRED - BS_STEP )) [ "${BS_LOWER}" -ge 1 ] && CANDIDATES+=("${BS_LOWER}") best_bs=1 best_fps_stream=0 best_ips=0 had_valid_ds_run=false for BS in "${CANDIDATES[@]}"; do echo "" echo "=== DS Confirmation BS=${BS} (${BS} streams) ===" # Write nvinfer config pointing to dynamic engine at this batch size BS_CONFIG="${CONFIGS_DIR}/config_infer_sweep_b${BS}.txt" sed \ -e "s|model-engine-file=.*|model-engine-file=${DYNAMIC_ENGINE}|" \ -e "s|batch-size=.*|batch-size=${BS}|" \ -e "s|custom-lib-path=.*|custom-lib-path=${PARSER_SO}|" \ -e "s|labelfile-path=.*|labelfile-path=${LABELS}|" \ "${CONFIG_TEMPLATE}" > "${BS_CONFIG}" # actual frames = ACTUAL_FRAMES_PER_STREAM × BS (no num-buffers limit on filesrc — # let each source read to natural EOS so we always process the full video) TOTAL_FRAMES=$((ACTUAL_FRAMES_PER_STREAM * BS)) # Build SOURCES as an array so a VIDEO path containing spaces or glob # characters survives shell expansion intact. SOURCES=() for ((i = 0; i < BS; i++)); do SOURCES+=( filesrc "location=${VIDEO}" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! "mux.sink_${i}" ) done START_TIME=$(date +%s%N) GST_RC=0 GST_DEBUG=0 gst-launch-1.0 -e \ "${SOURCES[@]}" \ nvstreammux name=mux batch-size=${BS} width=${MUXER_W} height=${MUXER_H} batched-push-timeout=40000 ! \ queue ! \ nvinfer config-file-path="${BS_CONFIG}" ! \ queue ! \ fakesink sync=0 2>>"${DS_ERR_LOG}" || GST_RC=$? END_TIME=$(date +%s%N) ELAPSED_NS=$(( END_TIME - START_TIME )) # Reject runs that exited non-zero or finished implausibly fast — both # indicate a plugin/config error rather than a real benchmark result. # Scoring such a run would produce divide-by-zero or a bogus high FPS. if [ "${GST_RC}" -ne 0 ] || [ "${ELAPSED_NS}" -lt "${NS_PER_SEC}" ]; then echo " [fail] gst-launch exit=${GST_RC} elapsed_ns=${ELAPSED_NS} — see ${DS_ERR_LOG}" >&2 continue fi had_valid_ds_run=true # Warn if the pipeline wrote anything to stderr — likely a plugin/config error if [ -s "${DS_ERR_LOG}" ]; then echo " [warn] GStreamer stderr output captured — see ${DS_ERR_LOG} for details" >&2 fi # awk instead of bc (bc is not installed in the DeepStream container; awk always is) ELAPSED_SEC=$(awk "BEGIN{printf \"%.2f\", ${ELAPSED_NS} / $NS_PER_SEC}") DS_IPS=$(awk "BEGIN{printf \"%.1f\", ${TOTAL_FRAMES} / ${ELAPSED_SEC}}") DS_FPS_STREAM=$(awk "BEGIN{printf \"%.1f\", ${DS_IPS} / ${BS}}") DS_REALTIME=$(awk "BEGIN{printf \"%.2f\", ${DS_FPS_STREAM} / ${FPS_THRESHOLD}}") DS_FPS_INT=$(echo "${DS_FPS_STREAM}" | cut -d. -f1) DS_IPS_INT=$(echo "${DS_IPS}" | cut -d. -f1) echo " BS=${BS}: wall=${ELAPSED_SEC}s imgs/s=${DS_IPS} fps/stream=${DS_FPS_STREAM} realtime=${DS_REALTIME}x" if [ "${DS_FPS_INT}" -ge "${FPS_THRESHOLD}" ]; then best_bs="${BS}" best_fps_stream="${DS_FPS_STREAM}" best_ips="${DS_IPS_INT}" echo " -> PASS (>=${FPS_THRESHOLD} fps/stream)" break # highest candidate that passes is BS_OPT else echo " -> FAIL (<${FPS_THRESHOLD} fps/stream), trying lower..." fi done # Abort if no candidate produced a valid DS run — emitting a default BS_OPT=1 # in this case would mislead the caller into building a production engine on # top of a broken sweep. if [ "${had_valid_ds_run}" != true ]; then echo "ERROR: no valid DeepStream confirmation run completed — see ${DS_ERR_LOG}" >&2 echo " refusing to emit ${ENGINES_DIR}/bs_opt.txt" >&2 exit 1 fi # Write results echo "" echo "======================================================" echo "SWEEP COMPLETE" echo " BS_OPT = ${best_bs}" echo " DS fps/stream = ${best_fps_stream} (threshold: ${FPS_THRESHOLD})" echo " DS imgs/sec = ${best_ips}" echo " trtexec alpha = ${ALPHA} (curve steepness)" echo " BS_pred was = ${BS_PRED}" echo "======================================================" cat > "${ENGINES_DIR}/bs_opt.txt" << EOF BS_OPT=${best_bs} DS_FPS_PER_STREAM=${best_fps_stream} DS_IPS=${best_ips} TRT_ALPHA=${ALPHA} TRT_A_COEFF=${A_COEFF} BS_PRED=${BS_PRED} EOF # Print probe summary echo "" echo "Phase 1 trtexec probe summary:" echo "batch,qps,imgs_per_sec" for i in "${!PROBE_BS_ARR[@]}"; do BS="${PROBE_BS_ARR[$i]}" QPS="${PROBE_QPS_ARR[$i]}" IPS=$(awk "BEGIN{printf \"%d\", ${QPS} * ${BS}}") echo "${BS},${QPS},${IPS}" done echo "${best_bs}" -
extract-frame.sh 1.9 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. set -o pipefail ################################################################################ # Step 6: Extract first frame from output video as PNG for visual inspection. # # Usage: ./extract-frame.sh <input_video> <output_png> # Example: ./extract-frame.sh yolox_output.mp4 yolox_frame_sample.png ################################################################################ INPUT="$1" OUTPUT="$2" if [ -z "$INPUT" ] || [ -z "$OUTPUT" ]; then echo "Usage: $0 <input_video> <output_png>" exit 1 fi if [[ "$INPUT" == *.ogv ]]; then gst-launch-1.0 \ filesrc location="${INPUT}" ! oggdemux ! theoradec ! videoconvert ! "video/x-raw,format=RGB" ! \ pngenc snapshot=true ! filesink location="${OUTPUT}" \ 2>&1 | grep -v "^$" else gst-launch-1.0 \ filesrc location="${INPUT}" ! qtdemux ! queue ! h264parse ! queue ! nvv4l2decoder ! queue ! \ nvvideoconvert ! "video/x-raw,format=RGB" ! videoconvert ! \ pngenc snapshot=true ! filesink location="${OUTPUT}" \ 2>&1 | grep -v "^$" fi STATUS=$? if [ $STATUS -eq 0 ] && [ -f "$OUTPUT" ]; then echo "Frame extracted: ${OUTPUT} ($(ls -lh "$OUTPUT" | awk '{print $5}'))" else echo "ERROR: Pipeline failed with exit code $STATUS" >&2 exit $STATUS fi
-
-
engine
-
benchmark-trtexec.sh 2.8 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. ################################################################################ # Step 8a: TensorRT benchmark using trtexec for arbitrary batch sizes. # Runs 10-second benchmarks and reports GPU compute time + throughput. # # Usage: ./benchmark-trtexec.sh <bs:engine> [<bs:engine> ...] [duration_sec] # Example: ./benchmark-trtexec.sh 1:yolox_nano_b1.engine 64:yolox_nano_b64.engine # ./benchmark-trtexec.sh 1:b1.engine 64:b64.engine 20 ################################################################################ # Last plain-integer arg is treated as duration; all others are bs:engine pairs. DURATION=10 ENGINE_PAIRS=() for arg in "$@"; do if [[ "$arg" =~ ^[0-9]+$ ]]; then DURATION="$arg" else ENGINE_PAIRS+=("$arg") fi done if [ ${#ENGINE_PAIRS[@]} -eq 0 ]; then echo "Usage: $0 <bs:engine> [<bs:engine> ...] [duration_sec]" echo " e.g. $0 1:model_b1.engine 64:model_b64.engine" exit 1 fi TRTEXEC="/usr/src/tensorrt/bin/trtexec" echo "=== TensorRT Benchmark ===" echo "Duration: ${DURATION}s per engine" echo "" for ENGINE_INFO in "${ENGINE_PAIRS[@]}"; do BATCH="${ENGINE_INFO%%:*}" ENGINE="${ENGINE_INFO#*:}" if [ ! -f "$ENGINE" ]; then echo "SKIP Batch ${BATCH}: ${ENGINE} not found" echo "" continue fi echo "--- Batch ${BATCH}: ${ENGINE} ---" OUTPUT=$($TRTEXEC --loadEngine="$ENGINE" --fp16 --duration="$DURATION" 2>&1) THROUGHPUT=$(echo "$OUTPUT" | grep "\[I\] Throughput:" | grep -oP 'Throughput: \K[0-9.]+') GPU_MEAN=$(echo "$OUTPUT" | grep "GPU Compute Time:" | grep -oP 'mean = \K[0-9.]+') GPU_MIN=$(echo "$OUTPUT" | grep "GPU Compute Time:" | grep -oP 'min = \K[0-9.]+') GPU_MAX=$(echo "$OUTPUT" | grep "GPU Compute Time:" | grep -oP 'max = \K[0-9.]+') IMGS_PER_SEC=$(awk "BEGIN{printf \"%d\", ${THROUGHPUT:-0} * $BATCH}" 2>/dev/null) # awk: bc absent in container echo " GPU Compute: ${GPU_MEAN} ms (min=${GPU_MIN}, max=${GPU_MAX})" echo " Throughput: ${THROUGHPUT} qps" echo " Images/sec: ${IMGS_PER_SEC}" echo "" done echo "=== Benchmark Complete ==="
-
-
model
-
cleanup.sh 2.7 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. # cleanup.sh — Remove build/models artifacts for a given model name. # Validated replacement for ad-hoc directory removal after ONNX export. # # Only removes paths that: # - are non-empty # - exist # - resolve under ./build/ or ./models/ # - match the given MODEL_NAME (regex-validated) # # Usage: # bash cleanup.sh <MODEL_NAME> [--dry-run] # # Example: # bash cleanup.sh yolov8n # bash cleanup.sh yolov8n --dry-run set -euo pipefail MODEL_NAME="${1:-}" DRY_RUN=false if [[ "${2:-}" == "--dry-run" ]]; then DRY_RUN=true fi if [[ -z "$MODEL_NAME" ]]; then echo "Usage: $0 <MODEL_NAME> [--dry-run]" >&2 exit 1 fi if ! [[ "$MODEL_NAME" =~ ^[A-Za-z0-9._-]+$ ]]; then echo "ERROR: MODEL_NAME must match ^[A-Za-z0-9._-]+$ (got: $MODEL_NAME)" >&2 exit 1 fi # The regex above accepts "." and ".." — reject them explicitly since those # would make the candidate paths (build/.venv_$MODEL_NAME, models/$MODEL_NAME/*) # point at directories we don't own. if [[ "$MODEL_NAME" == "." || "$MODEL_NAME" == ".." ]]; then echo "ERROR: MODEL_NAME cannot be '.' or '..' (got: $MODEL_NAME)" >&2 exit 1 fi CWD="$(pwd -P)" # Paths eligible for removal — all are scoped under CWD's build/ or models/ CANDIDATES=( "build/.venv_${MODEL_NAME}" "models/${MODEL_NAME}/hf_model" "models/${MODEL_NAME}/onnx_export" ) echo "=== cleanup.sh — MODEL_NAME=$MODEL_NAME dry-run=$DRY_RUN ===" for rel in "${CANDIDATES[@]}"; do abs="$CWD/$rel" if [[ ! -e "$abs" ]]; then echo " skip (not present): $rel" continue fi # Assert the resolved path is still under CWD's build/ or models/ resolved="$(cd "$(dirname "$abs")" && pwd -P)/$(basename "$abs")" case "$resolved" in "$CWD"/build/*|"$CWD"/models/*) ;; *) echo " SKIP (outside build/ or models/): $resolved" continue ;; esac if $DRY_RUN; then echo " [dry-run] rm -rf $resolved" else echo " removing: $resolved" rm -rf -- "$resolved" fi done echo "Done." -
config-to-labels.py 4.1 KB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"). """Gate the architecture and write labels.txt, from a model's config.json. Single implementation shared by both acquisition routes. The HuggingFace path (Step 2b) and the NGC path (Step 2d) reach config.json at different points but need identical behaviour, and keeping two copies in the runbook meant the NGC route silently drifted — it extracted labels without ever rejecting non-detection architectures, so a classification model could reach engine build. See references/model-acquire.md. Usage: python3 config-to-labels.py --config <config.json> --labels <labels.txt> python3 config-to-labels.py --config <config.json> --labels <labels.txt> --skip-arch-check Exits non-zero on a non-detection architecture or a missing label map. Callers must treat a non-zero exit as fatal: never fall back to COCO, ImageNet, or any other default list. """ from __future__ import annotations import argparse import json import sys from pathlib import Path # Detection heads this skill supports. DETR-family variants use the middle two. DETECTION_SUFFIXES = ("ForObjectDetection", "ForConditionalDetection", "ForZeroShotObjectDetection") NON_DETECTION_SUFFIXES = ( "ForImageClassification", "ForSemanticSegmentation", "ForInstanceSegmentation", "ForPanopticSegmentation", "ForDepthEstimation", "ForMaskedLM", "ForTokenClassification", "ForCausalLM", ) def assert_detection(cfg: dict) -> None: """Abort unless config.json declares an object-detection architecture.""" arch_list = cfg.get("architectures") or [] if not arch_list: print("[labels] config.json has no 'architectures' field; skipping architecture gate", file=sys.stderr) return arch = arch_list[0] if arch.endswith(NON_DETECTION_SUFFIXES) or not arch.endswith(DETECTION_SUFFIXES): sys.exit( f"ERROR: deepstream-import-vision-model currently supports object detection models " f"only. Detected architecture: {arch}. Classification, segmentation, and other vision " f"tasks are not yet supported." ) print(f"[labels] architecture OK: {arch}") def extract_labels(cfg: dict) -> list[str]: """Pull the class list, trying each known config layout in order.""" if "id2label" in cfg: # standard HF layout return [cfg["id2label"][str(i)] for i in range(len(cfg["id2label"]))] if "label2id" in cfg: # reversed map return [k for k, _ in sorted(cfg["label2id"].items(), key=lambda kv: kv[1])] if "names" in cfg: # some YOLO repos names = cfg["names"] return [names[str(i)] for i in range(len(names))] if isinstance(names, dict) else list(names) sys.exit("ERROR: No label map found in config.json — cannot create labels.txt") def main() -> None: ap = argparse.ArgumentParser(description="Validate architecture and emit labels.txt.") ap.add_argument("--config", required=True, help="path to config.json") ap.add_argument("--labels", required=True, help="path to write labels.txt") ap.add_argument("--skip-arch-check", action="store_true", help="emit labels without gating the architecture (diagnostics only)") args = ap.parse_args() try: cfg = json.loads(Path(args.config).read_text()) except (OSError, json.JSONDecodeError) as exc: sys.exit(f"ERROR: could not read {args.config}: {exc}") if not args.skip_arch_check: assert_detection(cfg) labels = extract_labels(cfg) out = Path(args.labels) out.parent.mkdir(parents=True, exist_ok=True) out.write_text("\n".join(labels) + "\n") print(f"labels.txt: {len(labels)} classes -> {out}") print(" " + ", ".join(labels[:5]) + (" ..." if len(labels) > 5 else "")) if __name__ == "__main__": main() -
hf-download-config.sh 2.2 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. # hf-download-config.sh — Download config.json from a HuggingFace repo. # Safer replacement for the inline `curl -fsSL ... -o ...` snippet. # # Usage: # bash hf-download-config.sh <HF_ORG> <MODEL_NAME> <DEST_PATH> # # Example: # bash hf-download-config.sh onnx-community yolov8n models/yolov8n/config/config.json # # Honors $HF_TOKEN if set. set -euo pipefail HF_ORG="${1:-}" MODEL_NAME="${2:-}" DEST="${3:-}" if [[ -z "$HF_ORG" || -z "$MODEL_NAME" || -z "$DEST" ]]; then echo "Usage: $0 <HF_ORG> <MODEL_NAME> <DEST_PATH>" >&2 exit 1 fi for arg_name in HF_ORG MODEL_NAME; do val="${!arg_name}" if ! [[ "$val" =~ ^[A-Za-z0-9._/-]+$ ]]; then echo "ERROR: $arg_name contains invalid characters: $val" >&2 exit 1 fi done # DEST must be a relative path and must not contain .. segments # (prevents writes outside the project tree) case "$DEST" in /*) echo "ERROR: DEST_PATH must be relative (absolute paths are rejected): $DEST" >&2 exit 1 ;; *..*) echo "ERROR: DEST_PATH contains '..' — refusing: $DEST" >&2 exit 1 ;; esac URL="https://huggingface.co/${HF_ORG}/${MODEL_NAME}/resolve/main/config.json" CURL_OPTS=(-fsSL --proto '=https' --tlsv1.2 --max-time 60 -o "$DEST") if [[ -n "${HF_TOKEN:-}" ]]; then CURL_OPTS+=(-H "Authorization: Bearer ${HF_TOKEN}") fi mkdir -p "$(dirname "$DEST")" if ! curl "${CURL_OPTS[@]}" "$URL"; then echo "ERROR: config.json not found at ${HF_ORG}/${MODEL_NAME} — cannot extract labels" >&2 exit 1 fi echo "Downloaded: $DEST" -
hf-list-files.sh 4.4 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. # hf-list-files.sh — List model files in a HuggingFace repo. # Uses the HF tree API with validated inputs, HTTPS+TLSv1.2, and a bounded # timeout. Parses the JSON response via the stdlib json module (no shell pipe). # # Usage: # bash hf-list-files.sh <HF_ORG> <MODEL_NAME> [subpath] # # Examples: # bash hf-list-files.sh onnx-community yolov8n # bash hf-list-files.sh onnx-community yolov8n onnx # check /onnx subdir # # Honors $HF_TOKEN if set (passed as Authorization: Bearer header). set -euo pipefail HF_ORG="${1:-}" MODEL_NAME="${2:-}" SUBPATH="${3:-}" if [[ -z "$HF_ORG" || -z "$MODEL_NAME" ]]; then echo "Usage: $0 <HF_ORG> <MODEL_NAME> [subpath]" >&2 exit 1 fi # Input validation — reject anything that could escape the URL for arg_name in HF_ORG MODEL_NAME SUBPATH; do val="${!arg_name:-}" if [[ -n "$val" ]] && ! [[ "$val" =~ ^[A-Za-z0-9._/-]+$ ]]; then echo "ERROR: $arg_name contains invalid characters (must match ^[A-Za-z0-9._/-]+\$): $val" >&2 exit 1 fi done URL="https://huggingface.co/api/models/${HF_ORG}/${MODEL_NAME}/tree/main" [[ -n "$SUBPATH" ]] && URL="${URL}/${SUBPATH}" # -sS: silent progress but still surface errors on stderr # -w "%{http_code}": append HTTP status as the last 3 chars of the response body # Drop -f so curl doesn't exit non-zero on 4xx — we inspect the status ourselves # so 404 (missing subpath) can be distinguished from network/auth failures. CURL_OPTS=(-sS --proto '=https' --tlsv1.2 --max-time 30 -w '%{http_code}') if [[ -n "${HF_TOKEN:-}" ]]; then CURL_OPTS+=(-H "Authorization: Bearer ${HF_TOKEN}") fi # Separate exit-code capture from body so we can diagnose failures precisely. CURL_RC=0 RESPONSE="$(curl "${CURL_OPTS[@]}" "$URL")" || CURL_RC=$? if [[ $CURL_RC -ne 0 ]]; then echo "ERROR: curl failed (exit $CURL_RC) while fetching $URL" >&2 exit 1 fi # -w appends the 3-digit status to the body; split them back apart. HTTP_CODE="${RESPONSE: -3}" JSON="${RESPONSE:0:${#RESPONSE}-3}" case "$HTTP_CODE" in 200) ;; # fall through to parsing 404) # Acceptable: the requested subpath (e.g. /onnx) doesn't exist. exit 0 ;; 401|403) echo "ERROR: HTTP $HTTP_CODE from HuggingFace for $URL (auth/permission)" >&2 exit 1 ;; *) echo "ERROR: HTTP $HTTP_CODE from HuggingFace for $URL" >&2 exit 1 ;; esac # 200 but empty body is unexpected — surface it rather than silently swallow. if [[ -z "$JSON" ]]; then echo "ERROR: HTTP 200 but empty body from $URL" >&2 exit 1 fi # Parse via python3 (json module is stdlib). Each line: <path> python3 - "$JSON" <<'PYEOF' import json, sys data = sys.argv[1] try: entries = json.loads(data) except json.JSONDecodeError as e: # Surface the decode error so callers can distinguish "empty repo" from # "HF returned something we can't parse" (upstream format change, captive # portal HTML, etc.). Truncate the raw data so we don't dump a multi-MB # response into logs. preview = data[:500] + ("... [truncated]" if len(data) > 500 else "") print(f"ERROR: failed to parse JSON from HuggingFace API: {e}", file=sys.stderr) print(f" raw response: {preview!r}", file=sys.stderr) sys.exit(1) if not isinstance(entries, list): preview = repr(entries)[:500] print( f"ERROR: unexpected response type from HuggingFace API: " f"{type(entries).__name__} (expected list)", file=sys.stderr, ) print(f" contents: {preview}", file=sys.stderr) sys.exit(1) # Empty list is valid (directory exists but has no files) — exit 0 silently. for e in entries: p = e.get("path") if isinstance(e, dict) else None if p: print(p) PYEOF -
inspect-onnx.py 3.7 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """ Step 1: Inspect an ONNX model — inputs, outputs, opset, operators, validity. Usage: python3 inspect-onnx.py <onnx_file> """ import sys import onnx if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} <onnx_file>") sys.exit(1) try: model = onnx.load(sys.argv[1]) except FileNotFoundError: print(f"Error: File '{sys.argv[1]}' not found") sys.exit(1) except Exception as e: print(f"Error loading ONNX model: {e}") sys.exit(1) print("=== ONNX Model Info ===") print(f"File: {sys.argv[1]}") opset_ver = model.opset_import[0].version if model.opset_import else "N/A" print(f"Opset: {opset_ver}") print(f"IR ver: {model.ir_version}") print(f"Producer: {model.producer_name} {model.producer_version}") graph = getattr(model, "graph", None) if graph is None: print("Error: ONNX model has no graph") sys.exit(1) print(f"Nodes: {len(graph.node)}") dtype_map = {1: "float32", 10: "float16", 7: "int64", 6: "int32", 9: "bool"} print("\n=== INPUTS ===") for inp in graph.input: shape = [d.dim_value if d.dim_value else d.dim_param for d in inp.type.tensor_type.shape.dim] dtype = dtype_map.get(inp.type.tensor_type.elem_type, inp.type.tensor_type.elem_type) print(f" {inp.name}: shape={shape}, dtype={dtype}") print("\n=== OUTPUTS ===") for out in graph.output: shape = [d.dim_value if d.dim_value else d.dim_param for d in out.type.tensor_type.shape.dim] dtype = dtype_map.get(out.type.tensor_type.elem_type, out.type.tensor_type.elem_type) print(f" {out.name}: shape={shape}, dtype={dtype}") print("\n=== Operators ===") op_types = sorted(set(n.op_type for n in graph.node)) print(f" {', '.join(op_types)}") print(f" Total unique ops: {len(op_types)}") try: onnx.checker.check_model(model) print("\n✓ ONNX model is valid") except Exception as e: print(f"\n✗ ONNX validation error: {e}") # --- Machine-parseable summary (consumed by the engine-build and pipeline-run phases) --- # grep patterns expect lines: "input_name: <name>", "height: <int>", "width: <int>" print("\n=== Machine-Parseable Summary ===") if graph and graph.input: inp = graph.input[0] dims = inp.type.tensor_type.shape.dim print(f"input_name: {inp.name}") if len(dims) >= 4: # Assume NCHW: dim[0]=batch, dim[1]=channels, dim[2]=H, dim[3]=W h_val = dims[2].dim_value # 0 means dynamic w_val = dims[3].dim_value if h_val > 0 and w_val > 0: print(f"height: {h_val}") print(f"width: {w_val}") else: # Dynamic spatial dims — print symbol so callers can detect failure print(f"height: DYNAMIC (symbol={dims[2].dim_param or 'unknown'})") print(f"width: DYNAMIC (symbol={dims[3].dim_param or 'unknown'})") print("WARNING: Dynamic H/W — set height and width manually in trtexec flags") else: print(f"WARNING: Input has {len(dims)} dims — expected 4 (NCHW); cannot auto-detect H/W") else: print("WARNING: No inputs found in ONNX graph") -
make-static-batch-onnx.py 2.7 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """ Step 7: Create static-batch ONNX files from a batch-1 ONNX. Patches input/output batch dims and internal Reshape nodes. Usage: python3 make-static-batch-onnx.py <src_onnx> <dst_onnx> <batch_size> Example: python3 make-static-batch-onnx.py yolox_nano.onnx b16/yolox_nano_b16.onnx 16 """ import sys import onnx import numpy as np from onnx import numpy_helper if len(sys.argv) != 4: print(f"Usage: {sys.argv[0]} <src_onnx> <dst_onnx> <batch_size>") sys.exit(1) src_path = sys.argv[1] dst_path = sys.argv[2] try: batch_size = int(sys.argv[3]) if batch_size <= 0: raise ValueError("Batch size must be positive") except ValueError as e: print(f"Error: Invalid batch size '{sys.argv[3]}': {e}") sys.exit(1) try: model = onnx.load(src_path) except FileNotFoundError: print(f"Error: File '{src_path}' not found") sys.exit(1) except Exception as e: print(f"Error loading ONNX model: {e}") sys.exit(1) graph = getattr(model, "graph", None) if graph is None: print("Error: ONNX model has no graph") sys.exit(1) # Set static batch on inputs for inp in graph.input: if len(inp.type.tensor_type.shape.dim) > 0: inp.type.tensor_type.shape.dim[0].dim_param = "" inp.type.tensor_type.shape.dim[0].dim_value = batch_size # Set static batch on outputs for out in graph.output: if len(out.type.tensor_type.shape.dim) > 0: out.type.tensor_type.shape.dim[0].dim_param = "" out.type.tensor_type.shape.dim[0].dim_value = batch_size # Fix Reshape nodes that reference batch=1 for node in graph.node: if node.op_type == "Reshape": shape_input = node.input[1] for init in graph.initializer: if init.name == shape_input: shape_data = numpy_helper.to_array(init).copy() if shape_data.size > 0 and shape_data[0] == 1: shape_data[0] = batch_size init.CopyFrom(numpy_helper.from_array(shape_data, name=init.name)) onnx.save(model, dst_path) print(f"Saved {dst_path} with batch={batch_size}") -
ngc-download.sh 3.8 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. # ngc-download.sh — Download all files from a public NGC model version. # Prefers the official `ngc` CLI. Falls back to authenticated HTTPS via curl # only when the CLI is not installed; the fallback is explicitly warned about. # # Usage: # bash ngc-download.sh <NGC_ORG> <NGC_TEAM> <MODEL_NAME> <NGC_VERSION> <DEST_DIR> # # Example: # bash ngc-download.sh nvidia tao peoplenet trainable_v2.6 models/peoplenet/ngc_download set -euo pipefail NGC_ORG="${1:-}" NGC_TEAM="${2:-}" MODEL_NAME="${3:-}" NGC_VERSION="${4:-}" DEST_DIR="${5:-}" if [[ -z "$NGC_ORG" || -z "$MODEL_NAME" || -z "$NGC_VERSION" || -z "$DEST_DIR" ]]; then echo "Usage: $0 <NGC_ORG> <NGC_TEAM> <MODEL_NAME> <NGC_VERSION> <DEST_DIR>" >&2 echo " NGC_TEAM may be empty-string if the model has no team segment." >&2 exit 1 fi for var in NGC_ORG MODEL_NAME NGC_VERSION; do val="${!var}" if ! [[ "$val" =~ ^[A-Za-z0-9._-]+$ ]]; then echo "ERROR: $var contains invalid characters: $val" >&2 exit 1 fi done if [[ -n "$NGC_TEAM" ]] && ! [[ "$NGC_TEAM" =~ ^[A-Za-z0-9._-]+$ ]]; then echo "ERROR: NGC_TEAM contains invalid characters: $NGC_TEAM" >&2 exit 1 fi case "$DEST_DIR" in ""|"/"|*..*) echo "ERROR: invalid DEST_DIR: $DEST_DIR" >&2 exit 1 ;; esac mkdir -p "$DEST_DIR" # Preferred: ngc CLI (authenticated, verified) if command -v ngc >/dev/null 2>&1 && ngc --version >/dev/null 2>&1; then if [[ -n "$NGC_TEAM" ]]; then SPEC="${NGC_ORG}/${NGC_TEAM}/${MODEL_NAME}:${NGC_VERSION}" else SPEC="${NGC_ORG}/${MODEL_NAME}:${NGC_VERSION}" fi echo "Using ngc CLI to download $SPEC -> $DEST_DIR" ngc registry model download-version "$SPEC" --dest "$DEST_DIR" exit 0 fi # Fallback: HTTPS via curl, public NGC catalog API only echo "WARNING: ngc CLI not available — falling back to unauthenticated HTTPS for public files." >&2 echo " For gated/private models, install the ngc CLI: https://ngc.nvidia.com/setup/installers/cli" >&2 if [[ -n "$NGC_TEAM" ]]; then NGC_BASE="https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${NGC_TEAM}/${MODEL_NAME}/versions/${NGC_VERSION}/files" else NGC_BASE="https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${MODEL_NAME}/versions/${NGC_VERSION}/files" fi SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" FILES="$("$SCRIPT_DIR/ngc-list-files.sh" "$NGC_ORG" "$NGC_TEAM" "$MODEL_NAME" "$NGC_VERSION")" if [[ -z "$FILES" ]]; then echo "ERROR: No files returned from NGC catalog" >&2 exit 1 fi echo "NGC files available:" echo "$FILES" while IFS= read -r FNAME; do [[ -z "$FNAME" ]] && continue # Skip anything with traversal characters case "$FNAME" in */..*|..*|*..|/*) echo " skipping suspicious filename: $FNAME" continue ;; esac DEST_PATH="$DEST_DIR/$FNAME" mkdir -p "$(dirname "$DEST_PATH")" echo "Downloading: $FNAME" if ! curl -fsSL --proto '=https' --tlsv1.2 --max-time 600 \ -o "$DEST_PATH" "${NGC_BASE}/${FNAME}"; then echo " WARNING: failed to download $FNAME — skipping" fi done <<< "$FILES" echo "Done. Files in $DEST_DIR:" ls -lh "$DEST_DIR" 2>/dev/null || true -
ngc-list-files.sh 2.7 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. # ngc-list-files.sh — List files in a public NGC model version. # Safer replacement for the inline curl+python snippet. # # Usage: # bash ngc-list-files.sh <NGC_ORG> <NGC_TEAM> <MODEL_NAME> <NGC_VERSION> # # Example: # bash ngc-list-files.sh nvidia tao peoplenet trainable_v2.6 # # Output: one filename per line. set -euo pipefail NGC_ORG="${1:-}" NGC_TEAM="${2:-}" MODEL_NAME="${3:-}" NGC_VERSION="${4:-}" if [[ -z "$NGC_ORG" || -z "$MODEL_NAME" || -z "$NGC_VERSION" ]]; then echo "Usage: $0 <NGC_ORG> <NGC_TEAM> <MODEL_NAME> <NGC_VERSION>" >&2 echo " NGC_TEAM may be empty-string if the model has no team segment." >&2 exit 1 fi for var in NGC_ORG MODEL_NAME NGC_VERSION; do val="${!var}" if ! [[ "$val" =~ ^[A-Za-z0-9._-]+$ ]]; then echo "ERROR: $var contains invalid characters: $val" >&2 exit 1 fi done if [[ -n "$NGC_TEAM" ]] && ! [[ "$NGC_TEAM" =~ ^[A-Za-z0-9._-]+$ ]]; then echo "ERROR: NGC_TEAM contains invalid characters: $NGC_TEAM" >&2 exit 1 fi if [[ -n "$NGC_TEAM" ]]; then NGC_BASE="https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${NGC_TEAM}/${MODEL_NAME}/versions/${NGC_VERSION}/files" else NGC_BASE="https://api.ngc.nvidia.com/v2/models/${NGC_ORG}/${MODEL_NAME}/versions/${NGC_VERSION}/files" fi if ! JSON="$(curl -fsSL --proto '=https' --tlsv1.2 --max-time 30 "${NGC_BASE}/" 2>/dev/null)"; then echo "ERROR: Could not retrieve file list from NGC API" >&2 echo "URL: ${NGC_BASE}/" >&2 exit 1 fi python3 - "$JSON" "${NGC_BASE}/" <<'PYEOF' import json, sys data, url = sys.argv[1], sys.argv[2] try: files = json.loads(data) except json.JSONDecodeError as e: print(f"ERROR parsing NGC file list: {e}", file=sys.stderr) sys.exit(1) if isinstance(files, list): names = [f.get("name", "") for f in files if isinstance(f, dict)] else: names = [f.get("name", "") for f in files.get("modelFiles", []) if isinstance(f, dict)] names = [n for n in names if n] if not names: print(f"ERROR: NGC API returned no model files at {url}", file=sys.stderr) sys.exit(1) for n in names: print(n) PYEOF -
resolve-engine.sh 1.8 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Apache-2.0. # # Resolve the engine that batch scaling actually produced, plus the values derived from it. # # WHY a helper: Step 6/7 (pipeline-run.md) and Step 8 (report-generation.md) both need the same # engine, and both used to inline the same lookup. They drifted — pipeline-run.md selected with # `head -1` and so picked b1 while report-generation.md picked the highest MAX_BS, meaning the # benchmark and the report could describe different engines. One implementation cannot drift. # # Selection rule: `sort -V | tail -1` takes the HIGHEST MAX_BS, i.e. the final engine from # iterative scaling. A lexicographic `head -1` would take b1 out of b1/b16/b8. # # USAGE — evaluate the output to set ENGINE / MAX_BS / MODEL_FILENAME in the caller's shell: # eval "$(bash .claude/skills/deepstream-import-vision-model/scripts/model/resolve-engine.sh "$MODEL_NAME")" # # Exits non-zero with a message on stderr when no engine is present. set -euo pipefail MODEL_NAME="${1:?usage: resolve-engine.sh <MODEL_NAME>}" ENGINE_DIR="models/${MODEL_NAME}/benchmarks/engines" ENGINE=$(ls "${ENGINE_DIR}"/*_dynamic_b*.engine 2>/dev/null | sort -V | tail -1 || true) if [ -z "$ENGINE" ]; then echo "ERROR: No engine found in ${ENGINE_DIR}/ — run Steps 4-5 first (references/engine-build.md)" >&2 exit 1 fi MAX_BS=$(echo "$ENGINE" | grep -oP '_b\K[0-9]+(?=\.engine)') MODEL_FILENAME=$(basename "$ENGINE" | sed 's/_dynamic_b[0-9]*\.engine//') printf 'ENGINE=%q\n' "$ENGINE" printf 'MAX_BS=%q\n' "$MAX_BS" printf 'MODEL_FILENAME=%q\n' "$MODEL_FILENAME" printf 'echo "Using engine: %s (MAX_BS=%s)"\n' "$ENGINE" "$MAX_BS" -
safetensors-to-onnx.sh 3.6 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. ################################################################################ # Step 1 (alternate): Convert SafeTensors model to ONNX. # # Uses the shared venv's torch.onnx.export via safetensors_to_onnx.py. The former # optimum-cli path was removed: optimum held transformers below 4.54.0, which blocked # the releases that fix its RCE advisories, and optimum 2.1.0 dropped the `onnx` # subcommand outright. # # Writes <output_dir>/model.onnx — the same filename optimum produced. # # Usage: ./safetensors-to-onnx.sh <hf_model_id_or_path> <output_dir> [extra args] # Extra args are passed through to safetensors_to_onnx.py, e.g. # --opset 17 · --image-size 640 · --max-batch 16 · --static-batch # Examples: # ./safetensors-to-onnx.sh PekingU/rtdetr_r50vd ./onnx_export # ./safetensors-to-onnx.sh facebook/detr-resnet-50 ./onnx_export --opset 17 # ./safetensors-to-onnx.sh ./local_model_dir ./onnx_export --image-size 800 ################################################################################ set -euo pipefail if [ $# -lt 2 ]; then echo "Usage: $0 <hf_model_id_or_path> <output_dir> [extra args for safetensors_to_onnx.py]" echo "" echo "Examples:" echo " $0 PekingU/rtdetr_r50vd ./onnx_export" echo " $0 facebook/detr-resnet-50 ./onnx_export --opset 17" exit 1 fi MODEL="$1" OUTPUT_DIR="$2" shift 2 EXTRA_ARGS=("$@") SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Installed layout is <root>/.claude/skills/deepstream-import-vision-model/scripts/model/, so the # working root (where setup.sh created build/.venv_optimum) is five levels up from this script. REPO_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" mkdir -p "$REPO_ROOT/build" VENV_DIR="$REPO_ROOT/build/.venv_optimum" VENV_PY="$VENV_DIR/bin/python" echo "=== SafeTensors → ONNX Export ===" echo "Model: $MODEL" echo "Output dir: $OUTPUT_DIR" echo "Extra args: ${EXTRA_ARGS[*]-}" echo "Venv: $VENV_DIR" echo "" # The venv is built ONCE, IN THE CONTAINER, by setup.sh — with virtualenv, because the DeepStream # container's python lacks ensurepip (so `python3 -m venv` would fail here). Reuse it; never recreate. if [ ! -x "$VENV_PY" ]; then echo "ERROR: venv not found at $VENV_DIR — run the one-time bootstrap first (in-container):" >&2 echo " docker run --rm -it --gpus all --shm-size=16g -v \"\$PWD\":/work -w /work \\" >&2 echo " --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\" >&2 echo " .claude/skills/deepstream-import-vision-model/setup.sh" >&2 exit 1 fi echo "Using venv: $VENV_DIR" echo "" # ${arr[@]+"${arr[@]}"} expands to nothing when the array is empty. Plain "${arr[@]-}" # would pass a single empty-string argument instead, which argparse rejects. "$VENV_PY" "$SCRIPT_DIR/safetensors_to_onnx.py" \ --model "$MODEL" --output-dir "$OUTPUT_DIR" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} echo "" echo "=== Export Complete ===" ls -lh "$OUTPUT_DIR"/*.onnx 2>/dev/null -
safetensors_to_onnx.py 10 KB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"). """SafeTensors -> ONNX export for object-detection models, via torch.onnx.export. Replaces the former `optimum-cli export onnx` path. optimum pinned transformers below 4.54.0, which made it impossible to reach the transformers releases that fix the RCE advisories (5.3.0 / 5.5.0); optimum 2.1.0 also dropped the `onnx` subcommand entirely. Scope is object detection, matching the skill. Every supported DETR-family detector (DETR, Conditional DETR, Deformable DETR, RT-DETR, YOLOS) exposes the same contract: one `pixel_values` input and `logits` + `pred_boxes` outputs. That uniformity is what makes a single generic exporter sufficient here. Writes `<output_dir>/model.onnx` — the same filename optimum produced, so downstream steps are unchanged. """ from __future__ import annotations import argparse import json import sys from pathlib import Path import torch from transformers import AutoConfig, AutoModelForObjectDetection DEFAULT_SIZE = 640 DETECTION_SUFFIXES = ("ForObjectDetection", "ForConditionalDetection", "ForZeroShotObjectDetection") def resolve_input_size(model_dir: str, override: int | None) -> tuple[int, int]: """Pick the export resolution: explicit override, else preprocessor_config.json, else 640.""" if override: return override, override cfg_path = Path(model_dir) / "preprocessor_config.json" if cfg_path.is_file(): try: size = json.loads(cfg_path.read_text()).get("size") or {} except (json.JSONDecodeError, OSError): size = {} if isinstance(size, dict): if "height" in size and "width" in size: return int(size["height"]), int(size["width"]) # DETR-style resize: a single shortest/longest edge. Export square at the # shortest edge — nvinfer feeds fixed-size letterboxed frames anyway. edge = size.get("shortest_edge") or size.get("longest_edge") if edge: return int(edge), int(edge) elif isinstance(size, int): return size, size return DEFAULT_SIZE, DEFAULT_SIZE def assert_detection_architecture(model_id: str, revision: str) -> None: """Fail fast on non-detection models, matching the skill's stated scope.""" try: config = AutoConfig.from_pretrained(model_id, revision=revision) except Exception as exc: # noqa: BLE001 - surface the loader's own message sys.exit(f"ERROR: could not read model config for {model_id!r}: {exc}") arch_list = getattr(config, "architectures", None) or [] if arch_list and not any(a.endswith(DETECTION_SUFFIXES) for a in arch_list): sys.exit( f"ERROR: deepstream-import-vision-model supports object detection models only. " f"Detected architecture: {arch_list[0]}. Classification, segmentation, and other " f"vision tasks are not supported." ) class DetectionWrapper(torch.nn.Module): """Reduce the HF output object to the two tensors DeepStream's parser consumes.""" def __init__(self, model: torch.nn.Module) -> None: super().__init__() self.model = model def forward(self, pixel_values: torch.Tensor): out = self.model(pixel_values=pixel_values) return out.logits, out.pred_boxes def consolidate_external_data(onnx_path: Path) -> None: """Fold any sidecar `.onnx.data` back into the model file. torch.onnx.export splits tensors out for large models; trtexec expects one file. """ sidecar = onnx_path.with_suffix(onnx_path.suffix + ".data") if not sidecar.exists(): return import onnx model = onnx.load(str(onnx_path), load_external_data=True) onnx.save(model, str(onnx_path)) sidecar.unlink() print(f"[export] consolidated external data ({sidecar.name}) into {onnx_path.name}") def verify(onnx_path: Path) -> bool: """Assert the DeepStream input/output contract; return whether batch stayed dynamic. Neither backend reliably honours a dynamic batch dimension: the dynamo exporter can specialize it where the model's own code captures `shape[0]`, so it is checked rather than assumed. """ import onnx model = onnx.load(str(onnx_path)) onnx.checker.check_model(model) inputs = {i.name: i for i in model.graph.input} outputs = [o.name for o in model.graph.output] print(f"[export] inputs={list(inputs)} outputs={outputs}") if "pixel_values" not in inputs: sys.exit(f"ERROR: exported graph has no 'pixel_values' input (got {list(inputs)})") for required in ("logits", "pred_boxes"): if required not in outputs: sys.exit(f"ERROR: exported graph is missing the '{required}' output (got {outputs})") dims = inputs["pixel_values"].type.tensor_type.shape.dim shape = [d.dim_param or d.dim_value for d in dims] print(f"[export] pixel_values shape={shape}") return bool(dims[0].dim_param) def export_dynamo(wrapper, h, w, onnx_path, opset, static_batch, max_batch) -> None: """torch.export-based exporter. Handles models the TorchScript tracer cannot.""" # Trace with batch=2 so torch.export cannot specialize the dimension to the constant 1. dummy = torch.randn(1 if static_batch else 2, 3, h, w) shapes = None if static_batch else { "pixel_values": {0: torch.export.Dim("batch", min=1, max=max_batch)}, } torch.onnx.export( wrapper, (dummy,), str(onnx_path), dynamic_shapes=shapes, dynamo=True, input_names=["pixel_values"], output_names=["logits", "pred_boxes"], opset_version=opset, ) def export_torchscript(wrapper, h, w, onnx_path, opset, static_batch, max_batch) -> None: """Legacy TorchScript exporter. Still the more reliable path for a dynamic batch dimension on DETR-family detectors — RT-DETR specializes batch under dynamo but stays dynamic here. It does fail on text-conditioned models (Grounding DINO and friends), which is what dynamo is for. """ axes = None if static_batch else { "pixel_values": {0: "batch"}, "logits": {0: "batch"}, "pred_boxes": {0: "batch"}, } torch.onnx.export( wrapper, torch.randn(1, 3, h, w), str(onnx_path), dynamic_axes=axes, do_constant_folding=True, dynamo=False, input_names=["pixel_values"], output_names=["logits", "pred_boxes"], opset_version=opset, ) def main() -> None: ap = argparse.ArgumentParser(description="Export a HuggingFace detection model to ONNX.") ap.add_argument("--model", required=True, help="HF model id or local model directory") ap.add_argument("--output-dir", required=True, help="directory to write model.onnx into") # 18, not 17: the dynamo exporter implements >=18 and auto-upgrades anyway, and its # downgrade path fails outright on Resize ("No Adapter To Version 17 for Resize"). # Opset 18 is compatible with TRT 10.16 — see references/engine-build.md. ap.add_argument("--opset", type=int, default=18, help="ONNX opset (default 18)") ap.add_argument("--image-size", type=int, default=None, help="square export size; default reads preprocessor_config.json, else 640") ap.add_argument("--max-batch", type=int, default=16, help="upper bound for the dynamic batch dimension (default 16)") ap.add_argument("--static-batch", action="store_true", help="export a fixed batch-1 graph instead of a dynamic batch dimension") ap.add_argument("--revision", default="main", help="Hub revision (branch, tag, or commit SHA). Pin a SHA for reproducible builds.") ap.add_argument("--legacy-torchscript", action="store_true", help="force the TorchScript exporter instead of trying dynamo first") args = ap.parse_args() assert_detection_architecture(args.model, args.revision) h, w = resolve_input_size(args.model, args.image_size) print(f"[export] model={args.model} input=1x3x{h}x{w} opset={args.opset}") model = AutoModelForObjectDetection.from_pretrained(args.model, revision=args.revision) # .eval() on the wrapper too — it is a fresh nn.Module and defaults to training mode, # which the exporter warns about and which changes dropout/batchnorm behaviour. wrapper = DetectionWrapper(model).eval() out_dir = Path(args.output_dir) out_dir.mkdir(parents=True, exist_ok=True) onnx_path = out_dir / "model.onnx" # Neither backend handles every architecture. Try dynamo first (it copes with models the # tracer chokes on), then fall back to TorchScript when dynamo specializes the batch # dimension — which is what RT-DETR, the default model, does. strategies = [("legacy-torchscript", export_torchscript)] if args.legacy_torchscript \ else [("dynamo", export_dynamo), ("legacy-torchscript", export_torchscript)] for index, (name, export_fn) in enumerate(strategies): last = index == len(strategies) - 1 print(f"[export] backend={name}") try: export_fn(wrapper, h, w, onnx_path, args.opset, args.static_batch, args.max_batch) except Exception as exc: # noqa: BLE001 - report and try the next backend if last: sys.exit(f"ERROR: {name} export failed: {exc}") print(f"[export] {name} failed ({type(exc).__name__}); trying the next backend") continue consolidate_external_data(onnx_path) if args.static_batch or verify(onnx_path): break if last: sys.exit( "ERROR: every backend baked in a static batch dimension. Re-run with " "--static-batch and build a fixed-batch engine for this model." ) print(f"[export] {name} produced a static batch dimension; trying the next backend") size_mb = onnx_path.stat().st_size / (1024 * 1024) print(f"[export] wrote {onnx_path} ({size_mb:.1f} MB)") if __name__ == "__main__": main()
-
-
report
-
generate-benchmark-charts.py 10.8 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """ Step 8: Generate exactly 5 benchmark charts as PNG images for the report. Usage: python3 generate-benchmark-charts.py <output_dir> <json_data_file> Expected JSON format (benchmark_data.json written by report-generation skill pre-flight): { "model_name": "yolo26_nano", "engine": "models/yolo26_nano/benchmarks/engines/yolo26n_dynamic_b256.engine", "max_bs": 256, "trtexec": { "bs1": {"qps": 220.5, "gpu_mean_ms": 4.53}, "bsmax": {"qps": 39.2, "gpu_mean_ms": 103.7, "p99_ms": 105.1, "imgs_per_sec": 10035.2} }, "peak_gpu_streams": 334, "deepstream": { "run1": {"streams": 334, "total_fps": 7850.0, "fps_per_stream": 23.5}, "run2": {"streams": 238, "total_fps": 7378.4, "fps_per_stream": 31.0} } } Outputs (fixed names — do not rename): chart_trtexec_bs1_vs_bsmax.png — grouped bar: QPS BS=1 vs BS=MAX_BS chart_trtexec_throughput.png — bar: imgs/sec at MAX_BS + PEAK_GPU_STREAMS annotation chart_ds_streams_vs_fps.png — line: stream count vs fps/stream, 30fps threshold chart_trt_vs_ds.png — grouped bar: trtexec vs DS Run1 vs DS Run2 total imgs/s chart_efficiency.png — bar: DS Run1 and Run2 pipeline efficiency % """ import sys import json import os import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt plt.rcParams.update({ 'figure.facecolor': 'white', 'axes.facecolor': '#FAFAFA', 'axes.grid': True, 'grid.alpha': 0.3, 'font.size': 11, 'axes.titlesize': 13, 'axes.titleweight': 'bold', }) COLORS = { 'blue': '#2196F3', 'green': '#4CAF50', 'orange': '#FF9800', 'pink': '#E91E63', 'purple': '#9C27B0', 'teal': '#00BCD4', 'red': '#FF5722', } def two_line_title(model_name, subtitle): """Two-line title: model name (line 1) + subtitle (line 2).""" return f'{model_name}\n{subtitle}' def chart_trtexec_bs1_vs_bsmax(data, output_dir): """Grouped bar chart: QPS at BS=1 vs BS=MAX_BS side by side.""" max_bs = data['max_bs'] qps_bs1 = data['trtexec']['bs1']['qps'] qps_bsmax = data['trtexec']['bsmax']['qps'] labels = ['BS=1', f'BS={max_bs}'] values = [qps_bs1, qps_bsmax] colors = [COLORS['blue'], COLORS['green']] fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.bar(labels, values, color=colors, width=0.5, edgecolor='white', linewidth=1.5) for bar, val in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + max(values) * 0.01, f'{val:.1f}', ha='center', va='bottom', fontweight='bold', fontsize=13) ax.set_ylabel('QPS (queries/sec)', fontsize=13) ax.set_ylim(0, max(values) * 1.18) ax.grid(axis='y', alpha=0.3) ax.set_title(two_line_title(data['model_name'], f'trtexec QPS: BS=1 vs BS={max_bs}')) plt.tight_layout() out = os.path.join(output_dir, 'chart_trtexec_bs1_vs_bsmax.png') fig.savefig(out, dpi=150) plt.close(fig) print(f' chart_trtexec_bs1_vs_bsmax.png') def chart_trtexec_throughput(data, output_dir): """Single bar: GPU-only imgs/sec at MAX_BS with PEAK_GPU_STREAMS annotation.""" max_bs = data['max_bs'] imgs_per_sec = data['trtexec']['bsmax']['imgs_per_sec'] peak_streams = data['peak_gpu_streams'] realtime_imgs = peak_streams * 30 # the throughput that satisfies peak_streams at 30fps fig, ax = plt.subplots(figsize=(10, 6)) bar = ax.bar([f'BS={max_bs}'], [imgs_per_sec], color=COLORS['blue'], width=0.4, edgecolor='white', linewidth=1.5) ax.text(bar[0].get_x() + bar[0].get_width() / 2, imgs_per_sec + imgs_per_sec * 0.01, f'{imgs_per_sec:.0f}', ha='center', va='bottom', fontweight='bold', fontsize=13) # Annotation line at PEAK_GPU_STREAMS × 30fps threshold ax.axhline(y=realtime_imgs, color=COLORS['red'], linestyle='--', linewidth=2, label=f'PEAK_GPU_STREAMS={peak_streams} × 30fps = {realtime_imgs:.0f} imgs/s') ax.text(0.98, realtime_imgs + imgs_per_sec * 0.01, f'PEAK={peak_streams} streams', ha='right', va='bottom', color=COLORS['red'], fontsize=10, fontweight='bold', transform=ax.get_yaxis_transform()) ax.set_ylabel('Images / sec', fontsize=13) ax.set_ylim(0, imgs_per_sec * 1.25) ax.grid(axis='y', alpha=0.3) ax.legend(loc='upper left', fontsize=10) ax.set_title(two_line_title(data['model_name'], f'GPU Throughput at BS={max_bs} (PEAK_GPU_STREAMS={peak_streams})')) plt.tight_layout() out = os.path.join(output_dir, 'chart_trtexec_throughput.png') fig.savefig(out, dpi=150) plt.close(fig) print(f' chart_trtexec_throughput.png') def chart_ds_streams_vs_fps(data, output_dir): """Line chart: X=stream count, Y=fps/stream. Red dashed line at 30fps.""" run1 = data['deepstream']['run1'] run2 = data['deepstream']['run2'] stream_counts = [run1['streams'], run2['streams']] fps_vals = [run1['fps_per_stream'], run2['fps_per_stream']] # Sort by stream count ascending pairs = sorted(zip(stream_counts, fps_vals)) stream_counts = [p[0] for p in pairs] fps_vals = [p[1] for p in pairs] fig, ax = plt.subplots(figsize=(10, 6)) ax.plot(stream_counts, fps_vals, color=COLORS['blue'], linewidth=2.5, marker='o', markersize=10, zorder=4) for sc, fp in zip(stream_counts, fps_vals): ax.text(sc, fp + max(fps_vals) * 0.025, f'{fp:.1f} fps', ha='center', va='bottom', fontweight='bold', fontsize=12) ax.axhline(y=30, color=COLORS['red'], linestyle='--', linewidth=2, label='30 fps/stream real-time threshold') ax.set_xlabel('Stream Count', fontsize=13) ax.set_ylabel('FPS / Stream', fontsize=13) lower = -max(fps_vals) * 0.15 ax.set_ylim(lower, max(fps_vals) * 1.3) ax.set_xticks(stream_counts) ax.grid(axis='y', alpha=0.3) ax.legend(loc='upper right', fontsize=10) # Label each point run_labels = {run1['streams']: 'Run 1\n(PEAK_GPU_STREAMS)', run2['streams']: 'Run 2\n(RT_STREAMS)'} for sc in stream_counts: ax.annotate(run_labels.get(sc, ''), xy=(sc, 0), xytext=(sc, -max(fps_vals) * 0.12), ha='center', fontsize=9, color='#555555') ax.set_title(two_line_title(data['model_name'], 'DeepStream: FPS/Stream vs Stream Count')) plt.tight_layout() out = os.path.join(output_dir, 'chart_ds_streams_vs_fps.png') fig.savefig(out, dpi=150) plt.close(fig) print(f' chart_ds_streams_vs_fps.png') def chart_trt_vs_ds(data, output_dir): """Grouped bars: trtexec total imgs/s | DS Run 1 total imgs/s | DS Run 2 total imgs/s.""" max_bs = data['max_bs'] trt_imgs = data['trtexec']['bsmax']['imgs_per_sec'] ds1_imgs = data['deepstream']['run1']['total_fps'] ds2_imgs = data['deepstream']['run2']['total_fps'] n1 = data['deepstream']['run1']['streams'] n2 = data['deepstream']['run2']['streams'] labels = [f'trtexec\nBS={max_bs}', f'DS Run 1\n({n1} streams)', f'DS Run 2\n({n2} streams)'] values = [trt_imgs, ds1_imgs, ds2_imgs] colors = [COLORS['pink'], COLORS['blue'], COLORS['green']] fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.bar(labels, values, color=colors, width=0.5, edgecolor='white', linewidth=1.5) for bar, val in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + max(values) * 0.01, f'{val:.0f}', ha='center', va='bottom', fontweight='bold', fontsize=13) ax.set_ylabel('Total Images / sec', fontsize=13) ax.set_ylim(0, max(values) * 1.18) ax.grid(axis='y', alpha=0.3) ax.set_title(two_line_title(data['model_name'], 'trtexec vs DeepStream: Total Throughput')) plt.tight_layout() out = os.path.join(output_dir, 'chart_trt_vs_ds.png') fig.savefig(out, dpi=150) plt.close(fig) print(f' chart_trt_vs_ds.png') def chart_efficiency(data, output_dir): """Bar chart: DS Run 1 and Run 2 pipeline efficiency %, dashed line at 100%.""" trt_imgs = data['trtexec']['bsmax']['imgs_per_sec'] ds1_imgs = data['deepstream']['run1']['total_fps'] ds2_imgs = data['deepstream']['run2']['total_fps'] n1 = data['deepstream']['run1']['streams'] n2 = data['deepstream']['run2']['streams'] if trt_imgs <= 0: print("ERROR: trtexec imgs_per_sec is zero or negative — cannot compute efficiency", file=sys.stderr) sys.exit(1) eff1 = round(ds1_imgs / trt_imgs * 100, 1) eff2 = round(ds2_imgs / trt_imgs * 100, 1) labels = [f'DS Run 1\n({n1} streams)', f'DS Run 2\n({n2} streams)'] values = [eff1, eff2] colors = [COLORS['purple'], COLORS['teal']] fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.bar(labels, values, color=colors, width=0.4, edgecolor='white', linewidth=1.5) ax.axhline(y=100, color='#333333', linestyle='--', linewidth=1.5, alpha=0.6, label='100% efficiency') for bar, val in zip(bars, values): ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5, f'{val}%', ha='center', va='bottom', fontweight='bold', fontsize=13) ax.set_ylabel('DS Efficiency (%)', fontsize=13) ax.set_ylim(0, max(values) * 1.2) ax.grid(axis='y', alpha=0.3) ax.legend(loc='upper right', fontsize=10) ax.set_title(two_line_title(data['model_name'], 'DeepStream Pipeline Efficiency vs trtexec')) plt.tight_layout() out = os.path.join(output_dir, 'chart_efficiency.png') fig.savefig(out, dpi=150) plt.close(fig) print(f' chart_efficiency.png') def main(): if len(sys.argv) != 3: print(f"Usage: {sys.argv[0]} <output_dir> <json_data_file>") sys.exit(1) output_dir = sys.argv[1] json_file = sys.argv[2] os.makedirs(output_dir, exist_ok=True) with open(json_file) as f: data = json.load(f) model = data.get('model_name', 'unknown') print(f"Generating 5 charts for {model} -> {output_dir}/") chart_trtexec_bs1_vs_bsmax(data, output_dir) chart_trtexec_throughput(data, output_dir) chart_ds_streams_vs_fps(data, output_dir) chart_trt_vs_ds(data, output_dir) chart_efficiency(data, output_dir) print("Done — 5 charts written.") if __name__ == "__main__": main() -
latex-pdf-wrap.tex 1.6 KB · in bundle
-
md-to-html-pdf.py 7.1 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """ Convert a GFM-style Markdown benchmark report to a styled HTML file and then to PDF via wkhtmltopdf. Images referenced as  are resolved relative to the markdown file's directory and embedded as base64 data URIs so the HTML is self-contained. Usage: python3 md-to-html-pdf.py <report.md> <style.css> <output_dir> [model_name] model_name (optional): if provided, PDF is named benchmark_report_{model_name}.pdf if omitted, derived from output_dir parent folder name Produces: <output_dir>/benchmark_report.html <output_dir>/benchmark_report_{model_name}.pdf """ import sys import os import re import base64 import shutil import subprocess import markdown def ensure_wkhtmltopdf(): """wkhtmltopdf is installed by setup.sh via apt, but that runs in an ephemeral --rm container so the binary does NOT persist to later per-phase containers (only the /work-mounted venv does). If it is missing, install it here so the PDF step works on a fresh run without a manual reinstall.""" if shutil.which('wkhtmltopdf'): return True apt = shutil.which('apt-get') if not apt: return False sudo = [] if os.geteuid() == 0 else (['sudo'] if shutil.which('sudo') else []) # Minimal environment rather than a copy of os.environ: apt-get needs only PATH and # HOME, and forwarding the full environment into a (possibly sudo-elevated) child # would carry loader hooks and apt overrides such as LD_PRELOAD or APT_CONFIG. env = { 'PATH': os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'), 'HOME': os.environ.get('HOME', '/root'), 'DEBIAN_FRONTEND': 'noninteractive', } print(" wkhtmltopdf missing — installing (in-container, one-time per container)...") for cmd in ([apt, 'update', '-qq'], [apt, 'install', '-y', '-qq', 'wkhtmltopdf']): subprocess.run(sudo + cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600) return shutil.which('wkhtmltopdf') is not None def embed_images(html: str, base_dir: str) -> str: """Replace <img src="file.png"> with base64-embedded data URIs.""" def replacer(match): prefix = match.group(1) src = match.group(2) suffix = match.group(3) # Skip URLs and absolute paths if re.match(r'^(https?|data|ftp)://', src) or os.path.isabs(src): return match.group(0) img_path = os.path.realpath(os.path.join(base_dir, src)) base_real = os.path.realpath(base_dir) # Reject path traversal outside base_dir if not img_path.startswith(base_real + os.sep) and img_path != base_real: return match.group(0) if os.path.isfile(img_path): ext = os.path.splitext(src)[1].lstrip('.').lower() mime = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'svg': 'image/svg+xml', 'gif': 'image/gif'}.get(ext, 'image/png') with open(img_path, 'rb') as f: b64 = base64.b64encode(f.read()).decode() return f'{prefix}data:{mime};base64,{b64}{suffix}' return match.group(0) return re.sub(r'(<img\s[^>]*src=["\'])([^"\']+)(["\'])', replacer, html) def main(): if len(sys.argv) not in (4, 5): print(f"Usage: {sys.argv[0]} <report.md> <style.css> <output_dir> [model_name]") sys.exit(1) md_path = sys.argv[1] css_path = sys.argv[2] out_dir = sys.argv[3] os.makedirs(out_dir, exist_ok=True) # Derive model name: explicit arg > parent-of-output_dir > "model" if len(sys.argv) == 5: model_name = sys.argv[4] else: # output_dir is typically models/{model_name}/reports/ — walk up two levels abs_out = os.path.abspath(out_dir) model_name = os.path.basename(os.path.dirname(abs_out)) or "model" base_dir = os.path.dirname(os.path.abspath(md_path)) with open(md_path, encoding='utf-8') as f: md_text = f.read() # Strip YAML frontmatter md_text = re.sub(r'^---\n.*?\n---\n', '', md_text, count=1, flags=re.DOTALL) with open(css_path, encoding='utf-8') as f: css = f.read() # Convert markdown to HTML html_body = markdown.markdown(md_text, extensions=['tables', 'fenced_code']) # Wrap in full HTML document html = f"""<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>DeepStream Benchmark Report — {model_name}</title> <style> {css} @media print {{ body {{ max-width: 100%; padding: 10px; }} img {{ max-width: 100%; page-break-inside: avoid; }} table {{ page-break-inside: avoid; }} h2 {{ page-break-after: avoid; }} }} </style> </head> <body> {html_body} </body> </html>""" # Embed images as base64 html = embed_images(html, base_dir) html_out = os.path.join(out_dir, 'benchmark_report.html') pdf_out = os.path.join(out_dir, f'benchmark_report_{model_name}.pdf') with open(html_out, 'w', encoding='utf-8') as f: f.write(html) print(f" HTML: {html_out}") # Ensure the PDF renderer is available (self-heal on fresh containers). if not ensure_wkhtmltopdf(): print(" PDF generation skipped: wkhtmltopdf unavailable and could not be installed. " "HTML report is complete.", file=sys.stderr) sys.exit(1) # Convert to PDF. # Intentionally NOT passing --enable-local-file-access: all images have already # been converted to base64 data: URIs by embed_images(), and the CSS is inlined # in <style>...</style>, so no file:// fetching is needed. Keeping it disabled # blocks a CSS/HTML-injection exfil vector if the upstream Markdown ever carries # untrusted content (e.g. an HF model card). result = subprocess.run( [ 'wkhtmltopdf', '--page-size', 'A4', '--margin-top', '15mm', '--margin-bottom', '15mm', '--margin-left', '15mm', '--margin-right', '15mm', '--image-quality', '100', '--no-outline', html_out, pdf_out, ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=False, timeout=300, ) if result.returncode == 0: print(f" PDF: {pdf_out}") else: print(f" PDF generation failed: {result.stderr[:500]}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main() -
md-to-pdf.sh 2.2 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 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. # Convert GitHub-Flavored Markdown (with optional Mermaid diagrams) to PDF with # correct wrapping: listings for code, Lua filter for tables/inline paths, LaTeX header. # # Usage: # ./md-to-pdf.sh <source.md> [output.pdf] # If output.pdf is omitted, writes <source>.pdf next to the source file. # # Requires: mmdc (Mermaid CLI), pandoc, pdflatex, packages: listings, xcolor, ragged2e. # # Do NOT replace this with plain "pandoc --highlight-style=..." — highlighted Verbatim # boxes do not wrap long lines; --listings + latex-pdf-wrap.tex + pandoc-wrap-tables.lua are required. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SRC_INPUT="${1:?Usage: $0 <markdown.md> [output.pdf]}" if [[ "$SRC_INPUT" != /* ]]; then SRC="$(cd "$(dirname "$SRC_INPUT")" && pwd)/$(basename "$SRC_INPUT")" else SRC="$SRC_INPUT" fi [[ -f "$SRC" ]] || { echo "error: file not found: $SRC" >&2; exit 1; } SRC_DIR="$(dirname "$SRC")" if [[ -n "${2-}" ]]; then OUT="$2" if [[ "$OUT" != /* ]]; then OUT="$(pwd)/$OUT" fi else OUT="${SRC%.md}.pdf" fi STEM="$(basename "$SRC" .md)" INTERMEDIATE="${SRC_DIR}/${STEM}._pdf.md" IMG_DIR="${SRC_DIR}/mermaid_pdf/${STEM}" python3 "$SCRIPT_DIR/render-mermaid-for-pdf.py" \ --img-dir "$IMG_DIR" \ "$SRC" \ "$INTERMEDIATE" pandoc "$INTERMEDIATE" \ --from=gfm \ --lua-filter="$SCRIPT_DIR/pandoc-wrap-tables.lua" \ --include-in-header="$SCRIPT_DIR/latex-pdf-wrap.tex" \ --pdf-engine=pdflatex \ -V geometry:margin=1in \ --listings \ --resource-path="$SRC_DIR:$SCRIPT_DIR" \ -o "$OUT" echo "Wrote $OUT" -
mermaid-puppeteer-root.json 86 B
{ "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] } -
mermaid-puppeteer.json 42 B
{ "args": ["--disable-dev-shm-usage"] } -
pandoc-wrap-tables.lua 2.2 KB · in bundle
-
render-mermaid-for-pdf.py 7.4 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """ Expand ```mermaid ... ``` blocks in a Markdown file into PNG images via mmdc, producing a new .md suitable for pandoc -> PDF. Does not modify the source file. Full PDF pipeline (see docs/md-to-pdf.sh and docs/build-pdf.sh): 1. This script: Mermaid -> PNG under docs/mermaid_pdf/<stem>/, replace blocks with  links. 2. pandoc --from=gfm --listings --lua-filter=pandoc-wrap-tables.lua --include-in-header=latex-pdf-wrap.tex --pdf-engine=pdflatex Use --listings (not --highlight-style): default highlighted Verbatim splits code into unbreakable tokens and overflows the page. The Lua filter wraps pipe tables and long path-like inline code; CodeBlock text is normalized for pdflatex (Unicode quotes, etc.). """ from __future__ import annotations import argparse import os import re import subprocess import sys from pathlib import Path MERMAID_BLOCK = re.compile( r"^```mermaid\s*\n(.*?)^```\s*$", re.MULTILINE | re.DOTALL, ) def render_one( mmdc: str, body: str, out_png: Path, width: int, scale: float, puppeteer_config: Path | None, ) -> None: out_png.parent.mkdir(parents=True, exist_ok=True) tmp = out_png.with_suffix(".mmd") tmp.write_text(body.strip() + "\n", encoding="utf-8") cmd = [ mmdc, "-i", str(tmp), "-o", str(out_png), "-e", "png", "-b", "white", "-w", str(width), "-s", str(scale), "-q", ] if puppeteer_config is not None: cmd.extend(["-p", str(puppeteer_config)]) r = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=False, timeout=120, ) tmp.unlink(missing_ok=True) if r.returncode != 0: sys.stderr.write(r.stderr or r.stdout or "mmdc failed\n") raise RuntimeError(f"mmdc failed with code {r.returncode}") def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("source", type=Path, help="Input .md path") ap.add_argument("output", type=Path, help="Output .md path") ap.add_argument( "--img-dir", type=Path, default=None, help="Directory for PNGs (default: next to output, mermaid_pdf/)", ) ap.add_argument("--mmdc", default="mmdc", help="Path to mmdc binary") ap.add_argument("--width", type=int, default=1100) ap.add_argument("--scale", type=float, default=1.5) ap.add_argument( "--puppeteer-config", type=Path, default=None, help="JSON for Puppeteer (default: mermaid-puppeteer.json next to this script)", ) args = ap.parse_args() # Optional: MERMAID_PDF_WIDTH / MERMAID_PDF_SCALE (e.g. build-pdf.sh for design doc) if os.environ.get("MERMAID_PDF_WIDTH"): args.width = int(os.environ["MERMAID_PDF_WIDTH"]) if os.environ.get("MERMAID_PDF_SCALE"): args.scale = float(os.environ["MERMAID_PDF_SCALE"]) script_dir = Path(__file__).resolve().parent # Two vetted Puppeteer configs ship alongside this script: # - mermaid-puppeteer.json : Chromium sandbox enabled. Used for # non-root execution (the secure # default for laptops, CI runners that # run as a non-root user, etc.). # - mermaid-puppeteer-root.json : --no-sandbox / --disable-setuid-sandbox. # Used only when this script runs as # uid 0, because Chromium refuses to # start with the setuid sandbox enabled # when running as root (common inside # container build environments). # Both configs also pass --disable-dev-shm-usage, which is a stability # workaround for small /dev/shm in containers (not a security flag). # # Selection is driven by the effective uid, never by user input. Any # --puppeteer-config that doesn't resolve to one of these two shipped # files is rejected. This prevents an attacker-supplied config from # introducing extra dangerous flags such as --remote-debugging-port # (would expose a control channel to the headless browser) or # --load-extension (would let arbitrary JS run in Chromium). sandboxed_pc = script_dir / "mermaid-puppeteer.json" root_pc = script_dir / "mermaid-puppeteer-root.json" is_root = hasattr(os, "geteuid") and os.geteuid() == 0 default_pc = root_pc if is_root else sandboxed_pc allowed = {p.resolve() for p in (sandboxed_pc, root_pc) if p.exists()} if args.puppeteer_config is not None: requested = args.puppeteer_config.resolve() if requested not in allowed: sys.stderr.write( "Refusing --puppeteer-config: only the shipped configs are " f"allowed ({sandboxed_pc.name}, {root_pc.name}). " f"Got: {requested}\n" ) sys.exit(2) default_pc = args.puppeteer_config puppeteer_config = default_pc if default_pc.is_file() else None if puppeteer_config is not None: uid_str = str(os.geteuid()) if hasattr(os, "geteuid") else "n/a" sys.stderr.write( f"[render-mermaid-for-pdf] using puppeteer config: " f"{puppeteer_config.name} (uid={uid_str})\n" ) # Validate source path exists and is a regular file if not args.source.is_file(): sys.stderr.write(f"ERROR: source markdown not found: {args.source}\n") sys.exit(1) text = args.source.read_text(encoding="utf-8") img_dir = args.img_dir if img_dir is None: img_dir = args.output.parent / "mermaid_pdf" n = 0 out_parent = args.output.parent.resolve() def repl(m: re.Match[str]) -> str: nonlocal n n += 1 body = m.group(1) png_name = f"diagram_{n:02d}.png" out_png = img_dir / png_name render_one( args.mmdc, body, out_png, args.width, args.scale, puppeteer_config, ) try: rel_to_md = out_png.resolve().relative_to(out_parent) except ValueError: # --img-dir is outside the output directory; fall back to os.path.relpath rel_to_md = Path(os.path.relpath(out_png.resolve(), out_parent)) return f"\n})\n" new_text, count = MERMAID_BLOCK.subn(repl, text) args.output.write_text(new_text, encoding="utf-8") if count: print(f"Rendered {count} Mermaid diagram(s) into {img_dir}", file=sys.stderr) if __name__ == "__main__": main() -
report-style.css 1.9 KB · in bundle
-
-
dsrun.sh 1.4 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Apache-2.0. # # Convenience wrapper: run a command INSIDE the DeepStream container with the working root # bind-mounted at /work. Every phase of this skill runs this way — nothing runs on the host. # # bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '<command to run in-container>' # e.g. bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh 'build/.venv_optimum/bin/python -c "import torch;print(torch.__version__)"' # bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh 'make -C models/yolov8n/parser && ls models/yolov8n/parser/*.so' # # Env overrides: DS_IMAGE (default DeepStream 9.1), DS_GPU (default "--gpus all", set "" for CPU-only steps). # On PowerShell the -v token is "${PWD}:/work"; on cmd "%cd%:/work" — Claude Code sets it per host shell. # See references/windows.md. set -euo pipefail IMG="${DS_IMAGE:-nvcr.io/nvidia/deepstream:9.1-triton-multiarch}" GPU="${DS_GPU-"--gpus all"}" if [ "$#" -eq 0 ]; then echo "usage: bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '<in-container command>'" >&2; exit 2; fi exec docker run --rm $GPU --shm-size=16g -v "$PWD":/work -w /work \ --entrypoint bash "$IMG" -lc "$*" -
preflight.sh 4.2 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. # Licensed under the Apache License, Version 2.0 (the "License"). # # Preflight for deepstream-import-vision-model: verify the environment BEFORE running the # import loop. Everything runs THROUGH the DeepStream container (no host packages). Checks: # 1. docker daemon reachable (host-mode only) # 2. the DeepStream image is pulled (host-mode only) # 3. GPU visible inside the container # 4. the venv has the ONNX-export + report packages, and trtexec is present # # Usage: # Host (Linux): bash scripts/preflight.sh # Through Docker (any OS — the portable way): # docker run --rm --gpus all -v "$PWD":/work -w /work --entrypoint bash \ # nvcr.io/nvidia/deepstream:9.1-triton-multiarch \ # .claude/skills/deepstream-import-vision-model/scripts/preflight.sh set -u IMG="${1:-nvcr.io/nvidia/deepstream:9.1-triton-multiarch}" VENV="${2:-build/.venv_optimum}" PKGS="torch torchvision transformers onnx onnxruntime onnxscript huggingface_hub matplotlib numpy markdown reportlab" fail=0 ok(){ echo " [OK] $1"; } warn(){ echo " [WARN] $1"; } bad(){ echo " [MISSING] $1"; fail=1; } # Container-mode: when run INSIDE the container (Windows/macOS invoke it as # docker run --gpus all -v <pwd>:/work -w /work <image> bash scripts/preflight.sh # ), the host docker/image checks are moot and there's no nested docker CLI — verify GPU + # venv + trtexec DIRECTLY. On a Linux host (no /.dockerenv) fall through to host-orchestration. if [ -f /.dockerenv ]; then echo "[preflight] container-mode (inside the container — verifying GPU + venv + trtexec directly)" echo "[preflight] 1/3 GPU (nvidia-smi)" nvidia-smi -L >/dev/null 2>&1 && ok "GPU visible in container" \ || bad "no GPU in container — run with --gpus all (Windows: Docker Desktop WSL2 backend + NVIDIA driver)" echo "[preflight] 2/3 trtexec" [ -x /usr/src/tensorrt/bin/trtexec ] && ok "trtexec present (/usr/src/tensorrt/bin/trtexec)" \ || bad "trtexec not found — is this the DeepStream/TensorRT image?" echo "[preflight] 3/3 python packages in $VENV" if [ -x "$VENV/bin/python" ]; then miss=$("$VENV/bin/python" -c "import importlib.util as u;print(' '.join(p for p in '$PKGS'.split() if u.find_spec(p) is None))" 2>/dev/null) if [ -z "${miss// /}" ]; then ok "all packages present" else bad "missing in venv: $miss (run setup.sh to (re)build the venv)"; fi else bad "venv not found: $VENV/bin/python — run setup.sh first (creates build/.venv_optimum)" fi echo "[preflight] RESULT: $([ $fail -eq 0 ] && echo 'PASS — environment ready' || echo 'FAIL — fix [MISSING] items before running')" exit $fail fi echo "[preflight] 1/4 docker daemon" docker info >/dev/null 2>&1 && ok "docker reachable" || bad "docker not installed or daemon not running" echo "[preflight] 2/4 DeepStream image: $IMG" have_img=0 if docker image inspect "$IMG" >/dev/null 2>&1; then ok "image present"; have_img=1 else warn "image not pulled — run: docker pull $IMG"; fi if [ "$have_img" = 1 ]; then echo "[preflight] 3/4 GPU via docker --gpus all" docker run --rm --gpus all --entrypoint nvidia-smi "$IMG" -L >/dev/null 2>&1 \ && ok "GPU visible in container" \ || bad "no GPU through --gpus all (check driver + nvidia-container-toolkit)" echo "[preflight] 4/4 python packages in $VENV" if [ -x "$VENV/bin/python" ]; then miss=$(docker run --rm --entrypoint /work/"$VENV"/bin/python -v "$PWD":/work "$IMG" \ -c "import importlib.util as u;print(' '.join(p for p in '$PKGS'.split() if u.find_spec(p) is None))" 2>/dev/null) if [ -z "${miss// /}" ]; then ok "all packages present" else bad "missing in venv: $miss (run setup.sh)"; fi else bad "venv not found: $VENV/bin/python — run setup.sh first" fi else echo "[preflight] 3/4 GPU — SKIPPED (image not pulled)" echo "[preflight] 4/4 python packages — SKIPPED (image not pulled)" fi echo "[preflight] RESULT: $([ $fail -eq 0 ] && echo 'PASS — environment ready' || echo 'FAIL — fix [MISSING] items before running')" exit $fail -
requirements.txt 2.6 KB
# Python dependencies for deepstream-import-vision-model (installed into build/.venv_optimum). # Run INSIDE nvcr.io/nvidia/deepstream:9.1-triton-multiarch via setup.sh — NOT on the host. # Release target: DeepStream 9.1 Triton image (Python 3.12); run the GPU validation matrix before signing. # # NOTE on torch: the default linux-x86_64 PyPI wheel for torch 2.13.0 bundles a CUDA 13 runtime # (nvidia-cudnn-cu13 / nccl-cu13 / cusparselt-cu13). It relies on NVIDIA driver compatibility rather # than the container CUDA toolkit; no special --index-url is needed. This replaces the previous # torch 2.6.0 / cu124 pin. 2.13.0 is the first torch release with zero open OSV advisories. # (Same torch/torchvision pins the sibling deepstream-eval-and-finetune skill uses.) # # VALIDATED end-to-end on 2026-08-04 in nvcr.io/nvidia/deepstream:9.1-triton-multiarch (H100): # setup.sh resolved and installed this exact set; SafeTensors -> ONNX -> TRT 10.16 FP16 # dynamic-shape engine built and PASSED trtexec for both # PekingU/rtdetr_r50vd (default model) 65.0 qps, 15.37 ms # hustvl/yolos-tiny 171.9 qps, 5.81 ms # ONNX Runtime confirmed a working dynamic batch dimension at batch 1/3/8. # Still to confirm on the full release matrix: Jetson/aarch64 and the non-H100 GPU lineup. # # NOTE on the export path: SafeTensors -> ONNX goes through scripts/model/safetensors_to_onnx.py # (direct torch.onnx.export), NOT optimum-cli. optimum was removed because it pinned transformers # below 4.54.0 (and optimum-onnx below 4.58.0), while the transformers RCE (GHSA-29pf-2h5f-8g72) # is fixed only in 5.3.0 and the model-init arbitrary code execution (GHSA-fgcw-684q-jj6r) only in # 5.5.0 — so no transformers 4.x release cleared them. optimum 2.1.0 also removed the `onnx` # subcommand entirely, making that path a dead end regardless. # --- ONNX export (SafeTensors -> ONNX, via torch.onnx.export) --- torch==2.13.0 torchvision==0.28.0 # torchvision 0.28.0 requires torch==2.13.0 exactly transformers==5.14.1 onnx==1.22.0 onnxruntime==1.27.0 onnxscript==0.7.0 # required by the torch.onnx.export dynamo backend huggingface_hub==1.26.0 # transformers 5.14.1 requires >=1.5.0,<2.0 tokenizers==0.22.2 # highest STABLE release inside transformers' >=0.22.0,<=0.23.0 range # (0.23.0 was never released as final; 0.23.1 exceeds the ceiling) safetensors==0.8.0 # transformers 5.14.1 requires >=0.8.0 # --- report: charts + markdown->HTML (PDF via wkhtmltopdf, apt-installed in the container by setup.sh) --- matplotlib==3.10.9 numpy==1.26.4 markdown==3.10.2 reportlab==4.5.1
-
-
tests
-
README.md 3 KB
# deepstream-import-vision-model tests Unit / validation tests for the hardened scripts in this skill. These run locally with only `python3` (stdlib + `bash`) — no GPU, no network, no external package install required. ## What is covered | Test class | Script | What it verifies | |---|---|---| | `TestInstallScript` | `install.sh` | `--target` rejects `/`, `..`, empty, missing; dry-run previews the self-contained skill copy without writing to target | | `TestCleanupScript` | `scripts/model/cleanup.sh` | `MODEL_NAME` regex enforcement; shell meta-chars / slashes rejected; dry-run does not touch real files | | `TestHFScripts` | `scripts/model/hf-list-files.sh`, `hf-download-config.sh` | `HF_ORG` / `MODEL_NAME` / `DEST` validation | | `TestNGCScripts` | `scripts/model/ngc-list-files.sh`, `ngc-download.sh` | NGC arg validation; `DEST_DIR` refuses `""`, `/`, path-traversal | | `TestKittiDumpUsage` | `scripts/deepstream/ds-kitti-dump.sh` | Usage message printed when required args missing | | `TestEmbedImages` | `scripts/report/md-to-html-pdf.py` | Local images inlined as `data:` URIs; remote / absolute / traversal paths left alone (proves `--enable-local-file-access` is safe to drop) | | `TestPowerShellInstaller` | `install.ps1` (**both** skills) | Every source file is installed — asserts an exact file-set match, not just that `SKILL.md` exists; Claude + Codex targets; `-NoCursor`; reinstall is idempotent and does not nest; the missing-`SKILL.md` guard throws | ### Note on `TestPowerShellInstaller` This one exists because of a shipped bug: `install.ps1` used `Copy-Item -LiteralPath $SkillDir -Destination $dest -Recurse`, whose behaviour depends on whether the destination already exists, and which on **Windows PowerShell 5.1** created the directory tree without copying any files — an empty skill dir, no `SKILL.md`, and a skill the agent runtime silently refused to load. Nothing caught it because the suites only ever exercised `install.sh`. It needs a PowerShell, discovered in this order: `pwsh`, `powershell`, then the `mcr.microsoft.com/powershell` docker image if already pulled. With none available it **skips** — it will never fail on a machine without PowerShell. To enable it on a Linux dev box: ```bash docker pull mcr.microsoft.com/powershell:latest ``` Caveat: the dev-box/container runner is PowerShell **7**. The original bug was 5.1-specific, so this pins the copy *contract* rather than reproducing every 5.1 quirk — run it on Windows for full fidelity. ## Run From the skill root: ```bash python3 -m unittest discover -s tests -v ``` Or a single class: ```bash python3 -m unittest tests.test_hardened_scripts.TestInstallScript -v ``` ## Adding tests Tests shell out to the real scripts and assert on exit codes + stderr/stdout, or import Python helpers directly. Keep them hermetic: use `tempfile.TemporaryDirectory()`, don't rely on network access, and don't shell out to tools that may not be installed on every dev machine (e.g. `trtexec`, `deepstream-app`, `wkhtmltopdf`, `mmdc`, `ngc`). -
test_evals_contract.py 10.5 KB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Contract tests for ``evals/evals.json``. Nothing in this repo validates the skill eval files, so every past defect had to be caught by a human or by a remote NVSkills-Eval run. The 2026-08-12 Litmus dispatch (run 6beb1477-c4c2-416c-b28d-77314215aebb) reported two classes of defect that these tests pin down: * **Negation-blind guards** — a ``not_contains_pattern`` that matches a bare identifier fires when the response *names the thing it is declining to do*, so a correct refusal is graded as if it performed the forbidden action. * **Credential false positives** — ``[A-Za-z0-9+/]{40,}`` matched ordinary HuggingFace ``resolve/main/...`` URLs and the string ``token-classification``. ``ANTI_NEGATION_CORPUS`` and ``BENIGN_STRINGS`` are the regression fixtures: every sentence in them is something a *correct* run legitimately emits, so no ``not_contains_pattern`` in the suite may match any of them. """ import json import re import unittest from pathlib import Path SKILL_ROOT = Path(__file__).resolve().parents[1] EVALS_PATH = SKILL_ROOT / "evals" / "evals.json" VALID_ASSERTION_TYPES = { "contains_pattern", "not_contains_pattern", "contains_phrase", } # Sentences a *correct* run emits. No negative guard may match any of these. ANTI_NEGATION_CORPUS = [ "gpt2 is a language model; I will not run trtexec or an engine build for nvinfer.", "This is out of scope, so I will not build a TensorRT engine or start a DeepStream pipeline.", "Local path detected - skipping huggingface_hub download entirely.", "The model is already on disk, so hf_hub_download is not needed.", "Multiple variants found; I automatically choose model_fp16.onnx without prompting.", "I will not ask which variant to use - the fp16-first rule selects it deterministically.", "To clarify, I will run the full import pipeline now without asking which skill applies.", "No custom parser compilation is needed: the built-in TAO parser in " "libnvds_infercustomparser.so is reused, so no g++ or cmake invocation is required.", "ResNet-50 is a classifier, so I am not going to run deepstream-app or gst-launch-1.0.", "You do not need to install the SDK with trtexec; apt-get installs the deb.", ] # Strings the skill legitimately prints that must never be flagged as a credential. BENIGN_STRINGS = [ "https://huggingface.co/onnx-community/yolov8n/resolve/main/onnx/model_fp16.onnx", "The config.json pipeline_tag is token-classification, so this is rejected.", "Export HF_TOKEN in your environment before running (never hardcode it).", "dataset digest sha256:a14983294ef15eedf15e8e8e68ae18da7cde7f1720e6634896c5f7c8b6ddbdc7", "Set TOKENIZERS_PARALLELISM=false to silence the warning.", "engine models/rtdetr_r50vd_dynamic_b16.engine built with trtexec --fp16", ] # Credential-shaped probes the guard must still catch. # # These are synthetic, but they cannot be written as literals: the repository secret # scanner reads source lines, so a fixture like the vendor prefix followed by 16 # characters is flagged here as if it were a live key. That is the same # false-positive class these tests exist to prevent, so each probe is split across a # concatenation boundary — no single literal below matches a secret pattern — and # padded with a zero-entropy filler. _PAD = "0" * 8 + "a" * 8 # 16 chars, no entropy def _probe(*fragments: str) -> str: """Join innocuous fragments into one credential-shaped string at runtime.""" return "".join(fragments) + _PAD SYNTHETIC_LEAKS = [ _probe("Authorization: ", "Bearer ", "hf", "_"), _probe("export ", "HF", "_TOKEN=", "hf", "_"), _probe("NGC", "_API", "_KEY=", "nvapi", "-"), _probe("openai key ", "sk", "-"), _probe("pass", "word: "), ] CREDENTIAL_ASSERTION_MARKER = "credential" def _load(): return json.loads(EVALS_PATH.read_text()) class TestEvalsSchema(unittest.TestCase): @classmethod def setUpClass(cls): cls.data = _load() cls.cases = cls.data["evals"] def test_skill_name_matches_directory(self): self.assertEqual(self.data["skill_name"], SKILL_ROOT.name) def test_ids_are_unique_non_empty_strings(self): ids = [c["id"] for c in self.cases] for case_id in ids: self.assertIsInstance(case_id, str, "eval ids must be strings") self.assertTrue(case_id.strip()) self.assertEqual(len(ids), len(set(ids)), "duplicate eval ids") def test_every_case_has_the_required_fields(self): for case in self.cases: with self.subTest(case=case["id"]): for field in ("name", "prompt", "expected_output", "assertions"): self.assertIn(field, case) self.assertTrue(case["assertions"], "case has no assertions") def test_assertion_types_are_known(self): for case in self.cases: for assertion in case["assertions"]: with self.subTest(case=case["id"], text=assertion["text"]): self.assertIn(assertion["type"], VALID_ASSERTION_TYPES) def test_every_regex_compiles(self): for case in self.cases: for assertion in case["assertions"]: if not assertion["type"].endswith("_pattern"): continue with self.subTest(case=case["id"], text=assertion["text"]): re.compile(assertion["pattern"]) class TestBehaviorGrading(unittest.TestCase): """Litmus grades activation from the transcript, not from prose.""" @classmethod def setUpClass(cls): cls.cases = _load()["evals"] def test_every_case_declares_should_trigger(self): for case in self.cases: with self.subTest(case=case["id"]): self.assertIn("should_trigger", case) self.assertIsInstance(case["should_trigger"], bool) def test_every_case_has_expected_behavior(self): for case in self.cases: with self.subTest(case=case["id"]): behaviors = case.get("expected_behavior") self.assertTrue(behaviors, "expected_behavior is missing or empty") for behavior in behaviors: self.assertIsInstance(behavior, str) self.assertTrue(behavior.strip()) def test_suite_contains_negative_cases(self): negatives = [c for c in self.cases if c["should_trigger"] is False] self.assertGreaterEqual(len(negatives), 3, "too few anti-trigger cases") def test_most_prompts_do_not_name_the_skill(self): """Discoverability must be measured, not assumed (PROMPT_NAMES_SKILL).""" named = [c for c in self.cases if SKILL_ROOT.name in c["prompt"]] self.assertLess( len(named), len(self.cases) / 2, "over half the prompts name the skill outright, so discoverability is untested", ) class TestNoNegationBlindGuards(unittest.TestCase): """The regression guard for the defect class Litmus reported. Each ``not_contains_pattern`` is run against sentences a correct run emits. A match means the assertion would fail the *right* answer. """ @classmethod def setUpClass(cls): cls.cases = _load()["evals"] def test_no_negative_guard_fires_on_a_correct_response(self): for case in self.cases: for assertion in case["assertions"]: if assertion["type"] != "not_contains_pattern": continue if CREDENTIAL_ASSERTION_MARKER in assertion["text"].lower(): continue pattern = re.compile(assertion["pattern"], re.IGNORECASE) for sentence in ANTI_NEGATION_CORPUS: with self.subTest(case=case["id"], text=assertion["text"]): match = pattern.search(sentence) if match is not None: self.fail( "negation-blind guard: matched " f"{match.group(0)!r} in a correct response: {sentence!r}" ) def test_negative_guards_are_anchored_to_an_invocation_shape(self): """A bare identifier is what makes a guard negation-blind.""" for case in self.cases: for assertion in case["assertions"]: if assertion["type"] != "not_contains_pattern": continue if CREDENTIAL_ASSERTION_MARKER in assertion["text"].lower(): continue pattern = assertion["pattern"] with self.subTest(case=case["id"], text=assertion["text"]): self.assertTrue( any(anchor in pattern for anchor in ("(^|\\n)", "\\?", "\\s")), "guard must be anchored to a line start, a question, or a " "command shape rather than matching a bare identifier", ) class TestCredentialGuard(unittest.TestCase): @classmethod def setUpClass(cls): cls.cases = _load()["evals"] cls.patterns = [ a["pattern"] for c in cls.cases for a in c["assertions"] if a["type"] == "not_contains_pattern" and CREDENTIAL_ASSERTION_MARKER in a["text"].lower() ] def test_the_suite_has_credential_guards(self): self.assertTrue(self.patterns) def test_all_credential_guards_are_identical(self): self.assertEqual( len(set(self.patterns)), 1, "credential guards have drifted apart; keep one shared pattern", ) def test_credential_guard_does_not_fire_on_benign_strings(self): pattern = re.compile(self.patterns[0], re.IGNORECASE) for benign in BENIGN_STRINGS: with self.subTest(benign=benign): match = pattern.search(benign) if match is not None: self.fail( f"false positive: matched {match.group(0)!r} in {benign!r}" ) def test_credential_guard_still_catches_real_leaks(self): pattern = re.compile(self.patterns[0], re.IGNORECASE) for leak in SYNTHETIC_LEAKS: with self.subTest(leak=leak): self.assertIsNotNone( pattern.search(leak), f"credential guard missed a leak: {leak!r}", ) if __name__ == "__main__": unittest.main() -
test_hardened_scripts.py 13.3 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """ Validation tests for the hardened scripts in deepstream-import-vision-model. Covers: - install.sh: --target validation (rejects /, .., empty, missing) dry-run previews self-contained skill copies - scripts/model/cleanup.sh: MODEL_NAME regex enforcement, empty arg rejection, dry-run does not touch the filesystem - scripts/model/hf-list-files.sh / hf-download-config.sh: rejects injection characters in HF_ORG / MODEL_NAME - scripts/model/ngc-list-files.sh / ngc-download.sh: rejects injection characters in NGC args refuses invalid DEST_DIR (empty, /, containing ..) - scripts/deepstream/ds-kitti-dump.sh: usage message printed when args missing - scripts/report/md-to-html-pdf.py::embed_images: base64-inlines local images, rejects path traversal, leaves absolute/remote URLs alone Run: python3 -m unittest discover -s tests -v """ from __future__ import annotations import base64 import os import shutil import subprocess import sys import tempfile import unittest from pathlib import Path SKILL_DIR = Path(__file__).resolve().parent.parent SCRIPTS = SKILL_DIR / "scripts" INSTALL_SH = SKILL_DIR / "install.sh" def run_script(cmd, cwd=None, timeout=30): """Run a command and return (returncode, stdout, stderr).""" r = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=False, cwd=str(cwd) if cwd else None, timeout=timeout, ) return r.returncode, r.stdout, r.stderr class TestInstallScript(unittest.TestCase): """install.sh TARGET validation (B6).""" def test_rejects_missing_target_arg(self): rc, out, err = run_script(["bash", str(INSTALL_SH)]) self.assertNotEqual(rc, 0) self.assertIn("--target is required", out + err) def test_rejects_filesystem_root(self): rc, out, err = run_script(["bash", str(INSTALL_SH), "--target", "/", "--dry-run"]) self.assertNotEqual(rc, 0) self.assertIn("invalid --target", out + err) def test_rejects_path_traversal(self): rc, out, err = run_script( ["bash", str(INSTALL_SH), "--target", "/tmp/../etc", "--dry-run"] ) self.assertNotEqual(rc, 0) self.assertIn("invalid --target", out + err) def test_rejects_nonexistent_target(self): rc, out, err = run_script( ["bash", str(INSTALL_SH), "--target", "/does/not/exist", "--dry-run"] ) self.assertNotEqual(rc, 0) self.assertIn("target directory not found", out + err) def test_dry_run_previews_all_skill_locations(self): with tempfile.TemporaryDirectory() as td: rc, out, _ = run_script( ["bash", str(INSTALL_SH), "--target", td, "--dry-run"] ) self.assertEqual(rc, 0, f"install.sh --dry-run failed: {out}") self.assertIn("[dry-run] cp -r", out) # Single skill copied as real files (references pattern — no sub-skills) self.assertIn(".claude/skills/deepstream-import-vision-model", out) self.assertNotIn("nv-model-acquire", out) self.assertNotIn("nv-engine-build", out) self.assertNotIn("ds-run-pipeline", out) self.assertNotIn("nv-import-vision-model-report", out) # Cursor skill also installed self.assertIn(".cursor/skills/deepstream-import-vision-model", out) self.assertIn(".codex/skills/deepstream-import-vision-model", out) # No agent directory — skill-only architecture self.assertNotIn(".claude/agents", out) # Nothing must be written in dry-run mode self.assertEqual(os.listdir(td), []) def test_reinstall_from_installed_path_is_safe(self): with tempfile.TemporaryDirectory() as td: installed = Path(td) / ".claude" / "skills" / "deepstream-import-vision-model" shutil.copytree(SKILL_DIR, installed) marker = installed / "SKILL.md" rc, out, err = run_script([ "bash", str(installed / "install.sh"), "--target", td, "--no-cursor", ]) self.assertEqual(rc, 0, f"stdout={out} stderr={err}") self.assertTrue(marker.exists(), "in-place reinstall must not delete its source") self.assertIn("source and destination are identical", out) class TestCleanupScript(unittest.TestCase): """scripts/model/cleanup.sh MODEL_NAME validation (B8).""" CLEANUP = SCRIPTS / "model" / "cleanup.sh" def test_rejects_missing_arg(self): rc, out, err = run_script(["bash", str(self.CLEANUP)]) self.assertNotEqual(rc, 0) self.assertIn("Usage", out + err) def test_rejects_shell_metachars(self): rc, _, err = run_script(["bash", str(self.CLEANUP), "bad;name", "--dry-run"]) self.assertNotEqual(rc, 0) self.assertIn("MODEL_NAME must match", err) def test_rejects_slash(self): rc, _, err = run_script(["bash", str(self.CLEANUP), "bad/name", "--dry-run"]) self.assertNotEqual(rc, 0) self.assertIn("MODEL_NAME must match", err) def test_accepts_valid_name_dry_run(self): with tempfile.TemporaryDirectory() as td: rc, out, err = run_script( ["bash", str(self.CLEANUP), "yolov8n", "--dry-run"], cwd=td ) self.assertEqual(rc, 0, f"stdout={out} stderr={err}") # No candidates exist in a fresh dir — all should be skipped, nothing removed self.assertIn("skip (not present)", out) def test_dry_run_does_not_remove_present_paths(self): with tempfile.TemporaryDirectory() as td: tdp = Path(td) target = tdp / "build" / ".venv_yolov8n" target.mkdir(parents=True) (target / "marker").write_text("present") rc, out, _ = run_script( ["bash", str(self.CLEANUP), "yolov8n", "--dry-run"], cwd=td ) self.assertEqual(rc, 0) self.assertIn("[dry-run]", out) self.assertTrue(target.exists(), "dry-run must not remove files") self.assertTrue((target / "marker").exists()) class TestHFScripts(unittest.TestCase): """HF helper script input validation (B4).""" LIST = SCRIPTS / "model" / "hf-list-files.sh" CONFIG = SCRIPTS / "model" / "hf-download-config.sh" def test_list_rejects_bad_org(self): rc, _, err = run_script(["bash", str(self.LIST), "bad;org", "model"]) self.assertNotEqual(rc, 0) self.assertIn("invalid characters", err) def test_list_rejects_bad_model(self): rc, _, err = run_script(["bash", str(self.LIST), "org", "bad$model"]) self.assertNotEqual(rc, 0) self.assertIn("invalid characters", err) def test_list_rejects_missing_args(self): rc, _, err = run_script(["bash", str(self.LIST)]) self.assertNotEqual(rc, 0) self.assertIn("Usage", err) def test_config_rejects_path_traversal_in_dest(self): rc, _, err = run_script( [ "bash", str(self.CONFIG), "org", "model", "relative/../etc/config.json", ] ) self.assertNotEqual(rc, 0) self.assertIn("'..'", err) def test_config_rejects_absolute_dest(self): rc, _, err = run_script( [ "bash", str(self.CONFIG), "org", "model", "/tmp/config.json", ] ) self.assertNotEqual(rc, 0) self.assertIn("must be relative", err) class TestNGCScripts(unittest.TestCase): """NGC helper script input validation (B5).""" LIST = SCRIPTS / "model" / "ngc-list-files.sh" DOWNLOAD = SCRIPTS / "model" / "ngc-download.sh" def test_list_rejects_bad_org(self): rc, _, err = run_script( ["bash", str(self.LIST), "bad;org", "team", "model", "v1"] ) self.assertNotEqual(rc, 0) self.assertIn("invalid characters", err) def test_download_rejects_empty_dest(self): rc, _, err = run_script( [ "bash", str(self.DOWNLOAD), "org", "team", "model", "v1", "", ] ) self.assertNotEqual(rc, 0) def test_download_rejects_traversal_dest(self): rc, _, err = run_script( [ "bash", str(self.DOWNLOAD), "org", "team", "model", "v1", "/tmp/../etc", ] ) self.assertNotEqual(rc, 0) self.assertIn("invalid DEST_DIR", err) def test_download_rejects_fs_root_dest(self): rc, _, err = run_script( [ "bash", str(self.DOWNLOAD), "org", "team", "model", "v1", "/", ] ) self.assertNotEqual(rc, 0) self.assertIn("invalid DEST_DIR", err) class TestKittiDumpUsage(unittest.TestCase): """scripts/deepstream/ds-kitti-dump.sh usage message (B7).""" KITTI = SCRIPTS / "deepstream" / "ds-kitti-dump.sh" def test_prints_usage_when_args_missing(self): rc, out, err = run_script(["bash", str(self.KITTI)]) self.assertNotEqual(rc, 0) combined = out + err self.assertTrue( "Usage" in combined or "unbound variable" in combined, f"expected Usage or unbound-variable error, got: {combined!r}", ) class TestEmbedImages(unittest.TestCase): """scripts/report/md-to-html-pdf.py::embed_images (B2). Images must be base64-inlined so wkhtmltopdf no longer needs --enable-local-file-access. """ @classmethod def setUpClass(cls): # Import the module by loading its source directly to avoid hyphen-path issues. # md-to-html-pdf.py imports the third-party `markdown` package at module scope, # which isn't needed for embed_images(). Stub it so tests run without that dep. import importlib.util import types if "markdown" not in sys.modules: stub = types.ModuleType("markdown") stub.markdown = lambda text, **kw: text # no-op for tests sys.modules["markdown"] = stub src = SCRIPTS / "report" / "md-to-html-pdf.py" spec = importlib.util.spec_from_file_location("md_to_html_pdf", src) cls.mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(cls.mod) def test_inlines_local_png(self): # 1x1 transparent PNG png = base64.b64decode( b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" ) with tempfile.TemporaryDirectory() as td: p = Path(td) (p / "pic.png").write_bytes(png) html = '<img src="pic.png" alt="x">' out = self.mod.embed_images(html, str(p)) self.assertIn("data:image/png;base64,", out) self.assertNotIn('src="pic.png"', out) def test_leaves_remote_url_alone(self): html = '<img src="https://example.com/pic.png" alt="x">' with tempfile.TemporaryDirectory() as td: out = self.mod.embed_images(html, td) self.assertEqual(html, out) def test_leaves_absolute_path_alone(self): html = '<img src="/opt/data/elsewhere.png" alt="x">' with tempfile.TemporaryDirectory() as td: out = self.mod.embed_images(html, td) # Should not be rewritten to a data: URI (absolute paths are skipped) self.assertNotIn("data:", out) self.assertIn('src="/opt/data/elsewhere.png"', out) def test_rejects_path_traversal(self): # src points outside base_dir — must not be embedded with tempfile.TemporaryDirectory() as td_base: with tempfile.TemporaryDirectory() as td_outside: secret = Path(td_outside) / "secret.png" secret.write_bytes(b"\x89PNG\r\n\x1a\nFAKE") rel = os.path.relpath(secret, td_base) # starts with ../ html = f'<img src="{rel}" alt="x">' out = self.mod.embed_images(html, td_base) self.assertNotIn("data:", out) self.assertIn(rel, out) if __name__ == "__main__": unittest.main(verbosity=2) -
test_onnx_export_contract.py 8.9 KB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Contract tests for the SafeTensors -> ONNX export path. These run without torch, a GPU, or any model download: they check the wrapper script's CLI contract and the exporter's static behaviour (input-size resolution, architecture gating), which is what actually breaks when the export path is refactored. The real export is exercised in-container against the DeepStream image; see references/model-acquire.md step 2b-iii. Run: python3 -m unittest discover -s tests -v """ from __future__ import annotations import ast import json import subprocess import tempfile import unittest from pathlib import Path SKILL_DIR = Path(__file__).resolve().parent.parent EXPORT_SH = SKILL_DIR / "scripts" / "model" / "safetensors-to-onnx.sh" EXPORT_PY = SKILL_DIR / "scripts" / "model" / "safetensors_to_onnx.py" REQUIREMENTS = SKILL_DIR / "scripts" / "requirements.txt" def _load_exporter_module(): """Import safetensors_to_onnx with torch/transformers stubbed. Those live only in the container venv. Stubs are installed into sys.modules just long enough for the import, then removed, so nothing leaks into other tests. """ import importlib import sys import types torch_stub = types.ModuleType("torch") nn_stub = types.ModuleType("torch.nn") class _Module: # stand-in base class for DetectionWrapper def __init__(self, *args, **kwargs): pass nn_stub.Module = _Module torch_stub.nn = nn_stub torch_stub.Tensor = object tf_stub = types.ModuleType("transformers") tf_stub.AutoConfig = object tf_stub.AutoModelForObjectDetection = object injected = {"torch": torch_stub, "torch.nn": nn_stub, "transformers": tf_stub} saved = {name: sys.modules.get(name) for name in injected} sys.modules.update(injected) module_dir = str(EXPORT_PY.parent) sys.path.insert(0, module_dir) try: sys.modules.pop("safetensors_to_onnx", None) return importlib.import_module("safetensors_to_onnx") finally: sys.path.remove(module_dir) sys.modules.pop("safetensors_to_onnx", None) for name, previous in saved.items(): if previous is None: sys.modules.pop(name, None) else: sys.modules[name] = previous class TestExportWrapperScript(unittest.TestCase): def test_wrapper_exists_and_parses(self): self.assertTrue(EXPORT_SH.is_file(), f"missing {EXPORT_SH}") rc = subprocess.run(["bash", "-n", str(EXPORT_SH)], capture_output=True, text=True) self.assertEqual(rc.returncode, 0, rc.stderr) def test_wrapper_rejects_missing_args(self): rc = subprocess.run(["bash", str(EXPORT_SH)], capture_output=True, text=True) self.assertNotEqual(rc.returncode, 0) self.assertIn("Usage:", rc.stdout + rc.stderr) def test_wrapper_errors_without_venv(self): """Without the container-built venv it must fail with bootstrap guidance, not a traceback.""" with tempfile.TemporaryDirectory() as td: rc = subprocess.run( ["bash", str(EXPORT_SH), "some/model", f"{td}/out"], capture_output=True, text=True, ) combined = rc.stdout + rc.stderr # Either the venv is genuinely absent (expected on a dev box) or it exists and the # export proceeds; only the absent case is asserted here. if rc.returncode != 0: self.assertIn("venv not found", combined) self.assertIn("setup.sh", combined) def test_wrapper_no_longer_invokes_optimum(self): text = EXPORT_SH.read_text() self.assertNotIn("optimum-cli export", text) self.assertIn("safetensors_to_onnx.py", text) class TestExporterModule(unittest.TestCase): """Static checks on the exporter — no torch import required.""" @classmethod def setUpClass(cls): cls.source = EXPORT_PY.read_text() cls.tree = ast.parse(cls.source) def test_module_parses(self): self.assertTrue(EXPORT_PY.is_file(), f"missing {EXPORT_PY}") def test_offers_both_export_backends(self): """Neither backend handles every architecture, so both must be present. dynamo copes with models the tracer chokes on; TorchScript is the one that keeps RT-DETR's batch dimension dynamic (dynamo specializes it to the trace batch). """ fns = {n.name for n in ast.walk(self.tree) if isinstance(n, ast.FunctionDef)} self.assertIn("export_dynamo", fns) self.assertIn("export_torchscript", fns) self.assertIn("dynamo=True", self.source) self.assertIn("dynamo=False", self.source) def test_each_backend_uses_its_own_dynamic_shape_parameter(self): """dynamic_shapes belongs to dynamo, dynamic_axes to TorchScript — mixing is a silent no-op.""" self.assertIn("torch.export.Dim", self.source) self.assertIn("dynamic_shapes", self.source) self.assertIn("dynamic_axes", self.source) def test_falls_back_when_a_backend_specializes_batch(self): """A static batch dim must trigger the next backend, not be accepted.""" self.assertIn("export_torchscript", self.source.split("strategies")[1]) self.assertIn("static batch dimension", self.source) def test_opset_defaults_to_18(self): """Opset 17 fails the dynamo downgrade pass: 'No Adapter To Version 17 for Resize'.""" self.assertRegex(self.source, r'"--opset".*?default=18') def test_declares_deepstream_output_contract(self): self.assertIn('"pixel_values"', self.source) self.assertIn('"logits"', self.source) self.assertIn('"pred_boxes"', self.source) def test_writes_model_onnx_filename(self): """Downstream steps copy models/<name>/onnx_export/model.onnx — keep that name.""" self.assertIn('"model.onnx"', self.source) def test_verifies_dynamic_batch_rather_than_assuming(self): """A backend can silently bake in a static batch dim; it must be checked, not assumed.""" fns = {n.name for n in ast.walk(self.tree) if isinstance(n, ast.FunctionDef)} self.assertIn("verify", fns) self.assertIn("consolidate_external_data", fns) # verify() reports whether the batch axis survived so the caller can switch backends. self.assertIn("dim_param", self.source) def test_rejects_non_detection_architectures(self): fns = {n.name for n in ast.walk(self.tree) if isinstance(n, ast.FunctionDef)} self.assertIn("assert_detection_architecture", fns) self.assertIn("ForObjectDetection", self.source) def test_input_size_resolution_handles_known_preprocessor_shapes(self): """Exercise resolve_input_size for real, importing the module with torch stubbed. The function is pure stdlib, but the module imports torch/transformers at top level, which are container-only. Stubbing sys.modules keeps this runnable on a dev box without resorting to dynamic code evaluation, which Tier-1 flags as AST1/AST8. """ resolve = _load_exporter_module().resolve_input_size with tempfile.TemporaryDirectory() as td: d = Path(td) self.assertEqual(resolve(str(d), 512), (512, 512), "explicit override wins") self.assertEqual(resolve(str(d), None), (640, 640), "no config -> 640 default") (d / "preprocessor_config.json").write_text( json.dumps({"size": {"height": 800, "width": 1333}})) self.assertEqual(resolve(str(d), None), (800, 1333), "height/width honoured") (d / "preprocessor_config.json").write_text( json.dumps({"size": {"shortest_edge": 800, "longest_edge": 1333}})) self.assertEqual(resolve(str(d), None), (800, 800), "shortest_edge -> square") (d / "preprocessor_config.json").write_text("{ not valid json") self.assertEqual(resolve(str(d), None), (640, 640), "malformed config falls back") class TestExportDependencies(unittest.TestCase): def test_optimum_is_gone(self): self.assertNotIn("optimum[exporters]", REQUIREMENTS.read_text()) def test_onnxscript_pinned_for_dynamo(self): """The dynamo ONNX backend needs onnxscript at runtime.""" self.assertRegex(REQUIREMENTS.read_text(), r"(?m)^onnxscript==") def test_transformers_past_the_rce_fixes(self): """GHSA-29pf-2h5f-8g72 is fixed in 5.3.0 and GHSA-fgcw-684q-jj6r in 5.5.0.""" import re m = re.search(r"(?m)^transformers==(\d+)\.(\d+)\.(\d+)", REQUIREMENTS.read_text()) self.assertIsNotNone(m, "transformers must be pinned") major, minor = int(m.group(1)), int(m.group(2)) self.assertGreaterEqual((major, minor), (5, 5), "transformers must be >= 5.5.0 to clear both HIGH RCE advisories") if __name__ == "__main__": unittest.main() -
test_powershell_installer.py 9.2 KB
#!/usr/bin/env python3 # SPDX-FileCopyrightText: Copyright (c) 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. """Regression tests for the Windows PowerShell installers of BOTH DeepStream skills. Why this exists --------------- `install.ps1` once shipped with: Copy-Item -LiteralPath $SkillDir -Destination $dest -Recurse -Force which is not portable. Whether it copies a folder's CONTENTS or the folder ITSELF depends on whether the destination already exists, and on Windows PowerShell 5.1 it created the directory tree without copying any leaf files -- installing an empty skill directory with no SKILL.md, so the agent runtime silently refused to load the skill. Nothing caught it, because the test suites only ever exercised install.sh. The central assertion here is therefore a strict **file-count match** against the source, not just "SKILL.md exists": a partial copy is exactly the failure mode that shipped. Runner ------ Needs a PowerShell. Discovery order: `pwsh`, `powershell`, then the `mcr.microsoft.com/powershell` container if docker has it locally (so this runs on Linux dev boxes and CI too). Skips cleanly when none is available -- never a false failure on a machine without PowerShell. Caveat: the container/dev-box runner is PowerShell 7. The bug that motivated this was 5.1-specific, so this pins the copy *contract* rather than reproducing every 5.1 quirk. Run it on Windows for full fidelity. Run: python3 -m unittest discover -s tests -v """ from __future__ import annotations import os import shutil import subprocess import tempfile import unittest from pathlib import Path REPO_SKILLS = Path(__file__).resolve().parents[2] PS_IMAGE = "mcr.microsoft.com/powershell:latest" # (skill dir name, expected file count key) -- both installers share the copy logic under test. SKILLS = ["deepstream-import-vision-model", "deepstream-eval-and-finetune"] # Machine-local build artifacts each installer strips from the copy; excluded when computing the # expected file count. Mirrors the strip rules inside the two install.ps1 scripts. STRIP_SUFFIXES = (".pyc", ".so", ".o") STRIP_NAMES = ("ds_image_eval",) def _docker_has_ps_image() -> bool: if not shutil.which("docker"): return False try: out = subprocess.run(["docker", "images", "-q", PS_IMAGE], capture_output=True, text=True, timeout=30) return bool(out.stdout.strip()) except Exception: return False def _runner(): """Return a callable(script, args, cwd) -> CompletedProcess, or None if no PowerShell.""" for exe in ("pwsh", "powershell"): if shutil.which(exe): def run_native(script, args, cwd, _exe=exe): return subprocess.run( [_exe, "-NoProfile", "-NonInteractive", "-File", str(script), *args], cwd=str(cwd), capture_output=True, text=True, timeout=300) return run_native if _docker_has_ps_image(): def run_docker(script, args, cwd): # Mount both trees at their real absolute paths so in-container paths match the host's # and the assertions below can read results back directly. `-File` (not `-Command`) so # that -Target/-NoCursor bind as named parameters rather than positional strings. return subprocess.run( ["docker", "run", "--rm", # Write as the invoking user, or the installed tree lands root-owned and the # caller cannot clean up its own temp dir. "--user", f"{os.getuid()}:{os.getgid()}", "-e", "HOME=/tmp", "-v", f"{REPO_SKILLS}:{REPO_SKILLS}:ro", "-v", f"{cwd}:{cwd}", "-w", str(cwd), PS_IMAGE, "pwsh", "-NoProfile", "-NonInteractive", "-File", str(script), *args], capture_output=True, text=True, timeout=600) return run_docker return None RUN = _runner() def _expected_files(src: Path) -> set[str]: """Files the installer should land, relative to the skill root, after its strip rules.""" out = set() for p in src.rglob("*"): if not p.is_file(): continue if "__pycache__" in p.parts: continue if p.suffix in STRIP_SUFFIXES or p.name in STRIP_NAMES: continue out.add(p.relative_to(src).as_posix()) return out @unittest.skipIf(RUN is None, "no PowerShell available (need pwsh/powershell on PATH, or the " f"{PS_IMAGE} docker image pulled)") class TestPowerShellInstaller(unittest.TestCase): def _install(self, skill, target, extra=()): # Copy the skill out of the repo first: the installer resolves $PSScriptRoot, and the repo # checkout is mounted read-only under the docker runner. src = Path(target) / "_src" / skill src.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(REPO_SKILLS / skill, src) proj = Path(target) / "proj" proj.mkdir() # --no-plugin equivalent: never touch the host's real ~/.claude during a test. args = ["-Target", str(proj), "-NoCursor", *extra] if skill == "deepstream-eval-and-finetune": args.append("-NoPlugin") res = RUN(src / "install.ps1", args, target) return res, proj, src def test_installs_every_source_file_including_skill_md(self): for skill in SKILLS: with self.subTest(skill=skill), tempfile.TemporaryDirectory() as td: res, proj, src = self._install(skill, td) self.assertEqual(res.returncode, 0, f"installer failed\nstdout={res.stdout}\nstderr={res.stderr}") dest = proj / ".claude" / "skills" / skill self.assertTrue((dest / "SKILL.md").is_file(), f"SKILL.md missing -> the skill would not load. stdout={res.stdout}") # The regression: an empty/partial tree used to pass silently. got = {p.relative_to(dest).as_posix() for p in dest.rglob("*") if p.is_file()} self.assertEqual(got, _expected_files(src), "installed file set does not match the source") def test_installs_for_codex_as_well_as_claude(self): for skill in SKILLS: with self.subTest(skill=skill), tempfile.TemporaryDirectory() as td: _, proj, _ = self._install(skill, td) for agent in (".claude", ".codex"): self.assertTrue((proj / agent / "skills" / skill / "SKILL.md").is_file(), f"{agent} install is missing SKILL.md") def test_no_cursor_flag_is_honoured(self): skill = SKILLS[0] with tempfile.TemporaryDirectory() as td: _, proj, _ = self._install(skill, td) self.assertFalse((proj / ".cursor").exists(), "-NoCursor still wrote .cursor/") def test_reinstall_is_idempotent_and_does_not_nest(self): for skill in SKILLS: with self.subTest(skill=skill), tempfile.TemporaryDirectory() as td: _, proj, src = self._install(skill, td) dest = proj / ".claude" / "skills" / skill first = {p.relative_to(dest).as_posix() for p in dest.rglob("*") if p.is_file()} args = ["-Target", str(proj), "-NoCursor"] if skill == "deepstream-eval-and-finetune": args.append("-NoPlugin") res = RUN(src / "install.ps1", args, td) self.assertEqual(res.returncode, 0, f"reinstall failed: {res.stderr}") again = {p.relative_to(dest).as_posix() for p in dest.rglob("*") if p.is_file()} self.assertEqual(first, again, "reinstall changed the installed file set") self.assertFalse((dest / skill).exists(), "reinstall nested the skill inside itself") def test_install_fails_loudly_when_skill_md_would_be_missing(self): """The guard must fire -- a silent partial install is the failure mode that shipped.""" skill = SKILLS[0] with tempfile.TemporaryDirectory() as td: src = Path(td) / "_src" / skill src.parent.mkdir(parents=True) shutil.copytree(REPO_SKILLS / skill, src) (src / "SKILL.md").unlink() proj = Path(td) / "proj" proj.mkdir() res = RUN(src / "install.ps1", ["-Target", str(proj), "-NoCursor"], td) self.assertNotEqual(res.returncode, 0, "installer reported success with SKILL.md absent") self.assertIn("SKILL.md", res.stdout + res.stderr) if __name__ == "__main__": unittest.main() -
test_skill_model_intake.py 2 KB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import re import unittest from pathlib import Path SKILL_ROOT = Path(__file__).resolve().parents[1] class TestSkillModelIntake(unittest.TestCase): @classmethod def setUpClass(cls): cls.skill = (SKILL_ROOT / "SKILL.md").read_text() cls.acquire = (SKILL_ROOT / "references" / "model-acquire.md").read_text() cls.choice = cls.skill.split( "## Model choice — always offer two options", 1 )[1].split("## Pipeline Overview", 1)[0] def test_exactly_two_ordered_model_choices_are_offered(self): self.assertEqual( re.findall(r"^### ([12])\. ", self.choice, flags=re.MULTILINE), ["1", "2"], ) def test_default_is_the_validated_rtdetr_model(self): self.assertIn("Default model (recommended)", self.choice) self.assertIn("PekingU/rtdetr_r50vd", self.choice) def test_custom_choice_accepts_hf_or_versioned_ngc(self): self.assertIn("Hugging Face model ID", self.choice) self.assertIn("NVIDIA NGC catalog model URL including its version", self.choice) self.assertIn("rejects classification, segmentation", self.choice) def test_model_acquire_runbook_repeats_the_choice_gate(self): intake = self.acquire.split("## Intake — choose the model", 1)[1].split( "## MANDATORY", 1 )[0] self.assertIn("Default model (recommended)", intake) self.assertIn("Custom object-detection model", intake) self.assertIn('INPUT="PekingU/rtdetr_r50vd"', intake) def test_dry_run_has_no_side_effects(self): normalized = " ".join(self.choice.split()) for phrase in ( "without browsing", "downloading", "launching Docker", "writing files", "starting processes", ): self.assertIn(phrase, normalized) if __name__ == "__main__": unittest.main() -
__init__.py 137 B
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0
-
-
.gitattributes 640 B · in bundle
-
.gitignore 46 B · in bundle
-
BENCHMARK.md 4.9 KB
# Skill Benchmark: deepstream-import-vision-model > ✅ **Overall verdict: PASS — Recommended for publication** ## Publication Recommendation Recommended for publication based on the completed evaluation evidence in this report. ## Evaluation Metadata - Skill: `deepstream-import-vision-model` - Evaluation date: 2026-08-13 - Evaluator version: `1.2.4` - Agents: Claude Code (`aws/anthropic/bedrock-claude-opus-4-8`), Codex (`openai/openai/gpt-5.5`) - Tasks: 13 evaluation tasks (13 positive) - Dataset digest: `sha256:09a6b1b8629eb5bcf468be26c1ee2aafdbd85a4b31fa501b9a6aab41a1ff76f1` (skill-evaluator-dataset-snapshot/1) - Attempts per task: 1 - Environment: `k8s-sandbox` - Tier 3 evidence: required for publication Each task attempt ran in its own isolated sandbox pod. ## What This Report Answers The three-tier evaluation checks whether the skill: - is safe to use; - produces correct answers; - is discovered and activated when needed; - helps the agent complete the user's goal and expected workflow; and - avoids wasted skill and tool usage. ## Results at a Glance | Measure | Claude Code (Baseline → Skill Uplift) | Codex (Baseline → Skill Uplift) | |---|---:|---:| | Overall | 44% → 72% (+27 points) | 46% → 61% (+15 points) | | Security | 58% → 54% (-4 points) | 35% → 42% (+8 points) | | Correctness | 42% → 86% (+45 points) | 65% → 72% (+8 points) | | Discoverability | 51% → 88% (+37 points) | 50% → 72% (+22 points) | | Effectiveness | 24% → 43% (+19 points) | 28% → 34% (+6 points) | | Efficiency | 48% → 86% (+38 points) | 54% → 84% (+30 points) | **How to read this table:** baseline is the same task attempted without the target skill. Uplift is `skill score - baseline score`, shown in percentage points. Example: `47% → 92% (+45 points)` means the skill-assisted run scored 92%, 45 percentage points above its 47% no-skill baseline. ## Tier Status | Tier | Purpose | Status | Evidence | |---|---|---|---| | Tier 1 | Static validation | **PASSED WITH OBSERVATIONS** | 1 validator(s); 5 finding(s) | | Tier 2 | Semantic deduplication | **NOT RUN** | No result was recorded | | Tier 3 | Live agent evaluation | **PASS** | 2 agent(s); 13 task(s) | ## Findings and Observations <details> <summary>Show detailed findings and successful checks</summary> - **MEDIUM** SCHEMA/body_recommended_section: Missing recommended section: '## Instructions' (`skills/deepstream-import-vision-model/SKILL.md`) - **LOW** SCHEMA/unexpected_file: Unexpected 'install.ps1' in skill root (`skills/deepstream-import-vision-model/install.ps1`) - **LOW** SCHEMA/unexpected_file: Unexpected 'install.sh' in skill root (`skills/deepstream-import-vision-model/install.sh`) - **LOW** SCHEMA/unexpected_file: Unexpected 'setup.sh' in skill root (`skills/deepstream-import-vision-model/setup.sh`) - **LOW** SCHEMA/unexpected_file: Unexpected 'CHANGELOG.md' in skill root (`skills/deepstream-import-vision-model/CHANGELOG.md`) </details> ## Scoring Methodology <details> <summary>Show dimension definitions, source signals, and thresholds</summary> | Dimension | Question | Scored signals | |---|---|---| | Security | Is it safe to use? | `security` (100%) | | Correctness | Is the answer correct? | `accuracy` (100%) | | Discoverability | Was the right skill loaded when needed? | `skill_execution` (100%) | | Effectiveness | Did the skill help complete the task? | `goal_accuracy` (50%) + `behavior_check` (50%) | | Efficiency | Did it avoid wasted tool or skill usage? | `skill_efficiency` (100%) | - Dimension bands: PASS at 50% or above; NEUTRAL from 40% to below 50%; FAIL below 40%. - Overall Tier 3 lift: PASS at +5 points or more; FAIL at -10 points or less; values between those bands are NEUTRAL. - Overall verdict: PASS only when every configured dimension passes for at least one supported agent. Lift is reported as diagnostic evidence and does not override this gate. - The 50% attempt pass threshold is a separate per-task gate; it is not the dimension pass threshold. - Effectiveness is the equal-weight mean of goal completion (`goal_accuracy`) and expected workflow adherence (`behavior_check`). - Token efficiency is a separate report-only signal. It does not change a dimension score or the overall verdict. Signals present in this run: - `security` (Security): unsafe operations, secret leakage, and unauthorized access. - `skill_execution` (Skill Execution): whether the expected skill was found and executed. - `skill_efficiency` (Efficiency): routing quality, workspace-aware skill reads, and productive tool use. - `accuracy` (Accuracy): final-answer correctness against the reference answer. - `goal_accuracy` (Goal Accuracy): whether the user's goal was achieved. - `behavior_check` (Behavior Check): whether the expected workflow behavior was followed. </details> ## Freshness Regenerate this benchmark when the skill, evaluation dataset, target agent/model, evaluator version, environment, or scoring policy changes. -
CHANGELOG.md 18.1 KB
# Changelog All notable changes to `deepstream-import-vision-model` are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/) and the skill uses [Semantic Versioning](https://semver.org/). ## [1.5.2] — 2026-08-05 ### Changed - Added `scripts/model/resolve-engine.sh` so Steps 6–7 and Step 8 share one engine resolver instead of each inlining the same glob, empty check, and `MAX_BS` parse. ### Fixed - Six defects found running the skill end-to-end on an H100 rather than by review. ## [1.5.1] — 2026-08-04 ### Changed - De-duplicated ONNX label extraction into a single helper shared by the HuggingFace and NGC routes. ## [1.5.0] — 2026-08-04 ### Fixed - Cleared the HIGH defects reported by SkillCritic across both DeepStream skills. ## [1.4.3] — 2026-08-04 ### Added - The missing `## Examples` section, with three concrete invocations: the default end-to-end run, a SafeTensors export showing the real dynamo → TorchScript fallback, and a revision-pinned build. ## [1.4.2] — 2026-08-04 ### Fixed - Cleared the remaining Tier-1 Code Risk Analysis findings, including B615 `huggingface_unsafe_download` — every `from_pretrained` / `snapshot_download` call now pins a revision. ## [1.4.1] — 2026-08-04 ### Fixed - Neutralised the NGC images' global `PIP_CONSTRAINT`, whose tested pins conflicted with this skill's dependency set and made pip fail with `ResolutionImpossible`. - Repaired transformers 5.x breakage in the export path. ## [1.4.0] — 2026-08-04 ### Changed - **Replaced `optimum-cli` with `torch.onnx.export`.** `optimum[exporters]` pinned transformers below 4.54.0, but the two HIGH RCE advisories (GHSA-29pf-2h5f-8g72, GHSA-fgcw-684q-jj6r) are only fixed in 5.3.0 and 5.5.0, so the pin was unfixable while optimum stayed. optimum 2.1.0 had also dropped the `onnx` subcommand, making that path a dead end regardless. The exporter now uses the dynamo backend with a TorchScript fallback and verifies the batch dimension stayed dynamic. ## [1.3.6] — 2026-08-04 ### Fixed - Cleared the NVSkills-Eval Tier-1 high-risk and Tier-2 findings that blocked the content gate, including the Agent Snooping findings in `install.sh`. ## [1.3.5] — 2026-07-29 ### Added - Explicit model-choice intake: the skill now always offers the validated default model and a custom object-detection model, and never silently substitutes one. ## [1.3.4] — 2026-07-28 ### Fixed - Updated the full workflow and container references to DeepStream 9.1 and CUDA 13.2. - Made Bash and PowerShell reinstallation safe when source and destination are identical. - Reconciled the README, phase references, tests, and eval expectations with the consolidated single-skill architecture. - Pinned the Python dependency stack used by the in-container setup. - Added standard Codex UI metadata and repository-required SPDX identifiers. ## [1.3.3] — 2026-07-20 ### Fixed - **`bc` dependency removed — timing/throughput math silently returned empty.** The DeepStream container has no `bc`, so every `$(echo "$A - $B" | bc)` produced an empty string with no error, leaving all pipeline-timing (and some throughput) values blank. Installing `bc` via `setup.sh` would not help — apt installs do not persist across the ephemeral `--rm` phase containers. All reference-doc timing now uses `python3 -c "print(round(...))"`; the helper scripts (`benchmark-ds.sh`, `ds-sweep.sh`, `benchmark-trtexec.sh`) use `awk "BEGIN{printf ...}"`. Both `python3` and `awk` are always present in the image. - **Step 8 PDF generation failed on a fresh run.** `setup.sh` installs `wkhtmltopdf` via apt inside an ephemeral `--rm` container, so the binary is gone by the time Step 8 runs in a later container (only the `/work`-mounted venv persists). `scripts/report/md-to-html-pdf.py` now self-heals via an `ensure_wkhtmltopdf()` helper that installs it if missing before rendering the PDF. The HTML (charts base64-inlined) was already unaffected. ### Changed - **Real-time stream selection now converges instead of halving.** When DS Run 2 came in marginally under 30 fps/stream, the old fallback *halved* RT_STREAMS (e.g. 38 streams @ 29.6 fps → 19), discarding ~half the GPU's real capacity and reporting a misleadingly low real-time count. Step 7 now recomputes the target from the measured throughput (`floor(TOTAL_FPS_RUN2 / 30)`) and steps down one stream at a time, landing on the true ceiling (e.g. 37) in 1–2 short retries. ## [1.3.2] — 2026-07-17 ### Removed - **`.claude-plugin/plugin.json`** — the skill now ships as a plain skill (like the sibling `deepstream-eval-and-finetune`), consistent with how it is installed by `install.sh` (whole-directory copy into `.claude/skills/` and `.cursor/skills/`) and used both standalone and bundled with other skills. The manifest was not referenced by `install.sh` and the skill was not registered in the repo marketplace, so removal has no effect on standalone or bundled use. This also lets NVCARPS nv-base classify the directory as `Type: skill` and run its Tier-3 live agent-eval (producing `BENCHMARK.md` + an `AGENT_EVAL` result), which the content gate requires and which the `Type: plugin` path skipped. To publish it later as a standalone marketplace plugin, re-add the manifest and run the signed marketplace flow. ## [1.3.1] — 2026-07-16 ### Added - **`install.ps1`** — native-Windows (PowerShell) installer twin of `install.sh`, with the identical sequence and flags (`-Target`=`--target`, `-NoCursor`=`--no-cursor`, `-DryRun`=`--dry-run`). Copies the skill into `<project>\.claude\skills\` (and `.cursor\skills\`). PowerShell 5.1+ compatible. ### Changed - `.gitattributes` forces LF on `*.ps1`; `references/windows.md` install note now points to `install.ps1`. ## [1.3.0] — 2026-07-16 ### Changed - **Runs entirely through Docker — no host packages.** Every step (venv/ONNX export, TensorRT engine build, nvinfer parser compile, DeepStream run, PDF report) now executes INSIDE the DeepStream container. The host needs only Docker + the NVIDIA driver, so the skill runs identically on Linux, **Windows** (Docker Desktop + WSL2 backend), and macOS. - Removed the host-native toolchain assumptions (host `trtexec`/`nvidia-smi`/`dpkg`/`make`/host venv/ `apt-get`); `wkhtmltopdf` + the export venv (`build/.venv_optimum`) are provisioned in-container by the new `setup.sh`. - **Reversed the "always build engines on the host" guidance** — build and run now share one image, so there is no TensorRT build-vs-runtime version skew (the exact failure the old rule tried to avoid). - `install.sh` now installs the **whole self-contained skill dir** (SKILL.md + references + scripts + setup.sh) into `.claude/skills/…`, dropping the separate `scripts/` tree and the `ln -sf` symlink path. ### Added - `setup.sh` (in-container bootstrap: venv + deps + wkhtmltopdf), `scripts/preflight.sh` (GPU + venv + trtexec, with container-mode), `scripts/requirements.txt`, `scripts/dsrun.sh` (docker wrapper), `.gitattributes` (LF), and `references/windows.md` (cross-platform runbook). ### Fixed - `scripts/model/safetensors-to-onnx.sh` no longer runs `python3 -m venv` (fails on the container python, which lacks ensurepip) — it reuses the virtualenv built by `setup.sh`. ## [1.2.2] — 2026-05-19 ### Changed - **Skill renamed** from `deepstream-byovm` to `deepstream-import-vision-model` across all files: `name:` in `SKILL.md` and `.claude-plugin/plugin.json`, package directory (`team-skills/deepstream-sdk/deepstream-import-vision-model/`), installed skill directories (`.claude/skills/deepstream-import-vision-model`, `.cursor/skills/deepstream-import-vision-model`), runtime scripts path (`scripts/deepstream-import-vision-model/`), invocation hints, eval prompts, tag (`byovm` → `import-vision-model`), README/title (`DS BYOVM` → `DeepStream Import Vision Model`), and cross-skill references in `team-skills/deepstream-sdk/README.md`, `team-skills/deepstream-sdk/deepstream-profile-pipeline/SKILL.md`, and `team-skills/deepstream-sdk/deepstream-profile-pipeline/README.md`. Body content (SKILL.md sections, `references/*.md`, and 5 differing scripts) was also resynced with the upstream `ds-copilot/skills/deepstream-import-vision-model` source - **Encoder fallback**: replaced `x264enc` fallback with `theoraenc + oggmux` (LGPL, outputs `.ogv`). `x264enc` and `openh264enc` are now prohibited (Rule 10). When neither NVENC nor `theoraenc`/`oggmux` is available, single-stream capture is skipped gracefully (`DS_SINGLE_STREAM_MODE=skipped`) - **Video source**: enforced `sample_720p.mp4` (1280×720) as the mandatory default; custom paths only via explicit `DS_VIDEO` (Rule 11) - **Performance measurement**: switched DS multi-stream benchmark from `gst-launch-1.0 ! fpsdisplaysink` (parsing `Current FPS:`) to `deepstream-app -c … enable-perf-measurement=1` (parsing `**PERF:` log lines) via the new `scripts/deepstream/ds-perf-run.sh` wrapper. Removes runtime dependency on `gstreamer1.0-plugins-bad` - **Media probing**: replaced `ffprobe` / `gst-discoverer` calls in `benchmark-ds.sh` and `ds-sweep.sh` with `mediainfo` (with safe fallbacks) - **Pipeline NVENC primary**: switched `nvvideoconvert` output format from `I420` to `NV12` ahead of `nvv4l2h264enc` - **Puppeteer sandbox**: split into two vetted configs — `mermaid-puppeteer.json` (sandboxed; non-root) and `mermaid-puppeteer-root.json` (sandbox disabled; only selected when `uid == 0`). `render-mermaid-for-pdf.py` auto-selects the right one and refuses any user-supplied config that does not resolve to one of these two shipped files (blocks `--remote-debugging-port`, `--load-extension`, etc.) - **`benchmark-trtexec.sh` interface**: replaced fixed `b1 b16 b32 b64` positional args with variadic `<bs:engine> [<bs:engine> …] [duration]` - **`ds-sweep.sh`**: input shape and tensor name are now derived dynamically via `inspect-onnx.py` instead of hardcoded `inputs` + `640×640` (fixes YOLOv8 / RT-DETR / DETR / non-YOLOX models). Power-law batch-size prediction is guarded against α≈0 (flat curves) - **Frame extraction**: `extract-frame.sh` now auto-detects `.mp4` vs `.ogv` and routes through the matching demux+decoder chain - **Custom parser filenames**: introduced `MODEL_NAME_SAFE = tr -c 'A-Za-z0-9' '_'` for `.cpp`/`.so` filenames so models like `rtdetr-l` produce a consistent `libnvdsinfer_rtdetr_l_parser.so` - **nvinfer config**: moved `cluster-mode` inline `#` comments to their own lines in both heredocs (GKeyFile rejects inline `#`) - **`make-static-batch-onnx.py`**: use `onnx.numpy_helper.to_array` / `from_array` for Reshape initializer patching (the old raw-bytes path silently skipped `int64_data` initializers, leaving `batch=1` baked in) - **Report verification**: replaced `>500 KB` heuristic with deterministic `grep -o 'data:image/png' benchmark_report.html | wc -l == 5` - **System tools**: pre-flight now installs `mediainfo` and checks for `deepstream-app` (instead of `gstreamer1.0-plugins-bad`) ### Added - `scripts/deepstream/ds-perf-run.sh` — wraps `deepstream-app` with `enable-perf-measurement=1`, emits `**PERF:` log lines for the report parser - `scripts/report/mermaid-puppeteer-root.json` — vetted root-only Puppeteer config - New SKILL.md table rows: `ds-perf-run.sh`, `md-to-pdf.sh`, `mermaid-puppeteer-root.json` ### Fixed - `ds-kitti-dump.sh`: added `set -euo pipefail`, replaced manual `rm -f` with trap-based cleanup, guarded `timeout` pipeline with `set +o pipefail` to preserve `PIPESTATUS` - `safetensors-to-onnx.sh`: added missing `set -euo pipefail` - `generate-benchmark-charts.py`: removed unused `import math` ## [1.2.1] — 2026-04-24 ### Changed - **Skill renamed** from `ds-byovm` to `deepstream-byovm` across all files: `name:` in `SKILL.md` and `plugin.json`, installed skill directory (`.claude/skills/deepstream-byovm`), runtime scripts path (`scripts/deepstream-byovm/`), invocation hints, eval prompts, and all cross-references in `references/*.md` ## [1.2.0] — 2026-04-24 ### Changed - **References pattern**: removed 4 standalone sub-skills (`nv-model-acquire`, `nv-engine-build`, `ds-run-pipeline`, `nv-byovm-report`); their content is now in `skills/ds-byovm/references/` (4 .md files) matching the ds-copilot `deepstream-dev` convention of single skill + reference documents - **Single skill dir**: `SKILL.md` moved from package root into `skills/ds-byovm/SKILL.md` (lean ~170 lines); root `SKILL.md` removed - **plugin.json**: `"skills": "./"` → `"skills": "skills/ds-byovm/"` to point at the skill directory instead of the package root - **install.sh**: creates one symlink (`skills/ds-byovm/` → `.claude/skills/ds-byovm` and `.cursor/skills/ds-byovm`) instead of 5; no sub-skill symlinks - **Installed structure** is now: ```text .claude/skills/ds-byovm/ SKILL.md references/ model-acquire.md engine-build.md pipeline-run.md report-generation.md scripts/ds-byovm/ (19 scripts, unchanged) ``` - **Tests**: updated install dry-run assertions for single-skill structure; sub-skill name assertions removed; `assertNotIn` for sub-skill names added ## [1.1.0] — 2026-04-24 ### Changed - **Skill-only architecture**: removed `agents/deepstream-sdk/ds-byovm.md`; top-level `SKILL.md` now serves both Claude Code and Cursor (Cursor does not support agents — skill is the correct primitive for cross-tool compatibility) - **Sub-skills renamed** with `nv-`/`ds-` prefix for namespace clarity: - `hf-model-acquire` → `nv-model-acquire` - `trt-engine-build` → `nv-engine-build` - `ds-integration` → `ds-run-pipeline` - `benchmark-report` → `nv-byovm-report` - **SKILL.md enhanced** (version 1.0.1 → 1.1.0): merged pre-flight checks, mandatory model folder structure, engine naming convention, run budget table, pipeline timing pattern, and report output convention from the removed agent doc - **install.sh updated**: removed agent installation block, updated symlink targets to new sub-skill names; invocation hints say "skill" not "agent" - **README.md updated**: skill-only usage section for Claude Code + Cursor; sub-skill standalone invocation documented; all agent references removed - **evals.json**: all prompts and assertion text updated from "agent" to "skill" - **Tests expanded**: dry-run test now asserts all 5 skill names, Cursor skills presence, and absence of `.claude/agents/` directory - **Shebang consistency**: all shell scripts use `#!/usr/bin/env bash` for portability across container images and macOS environments - **Local `.gitignore`**: added skill-level `.gitignore` for portability when placed in repos that do not inherit team-mind-hub's root `.gitignore` ## [1.0.1] — 2026-04-23 ### Fixed - `ds-integration` Step 6g KITTI dump produced zero detection files. `gie-kitti-output-dir` is a `deepstream-app` `[application]` key — it is not read by `nvinfer`, so appending it to the nvinfer config and running a `gst-launch-1.0 ... nvinfer ...` pipeline silently wrote no files. Step 6g now invokes `scripts/ds-byovm/deepstream/ds-kitti-dump.sh`, which wraps `deepstream-app` with the correct `[application]` section. ### Changed - `hf-model-acquire` Step 2b now uses a **single shared** `build/.venv_optimum` for SafeTensors → ONNX export across all models, matching what `scripts/ds-byovm/model/safetensors-to-onnx.sh` already does. The previous prose created a fresh `build/.venv_$MODEL_NAME` per model, which re-installed `optimum`/`transformers`/`torch` every run (~minutes + GBs wasted). New models that need extra packages (`timm` for DETR, `onnxsim`, etc.) should `pip install` into the shared venv. `cleanup.sh` still removes any legacy per-model venvs for backward compatibility, and explicitly preserves the shared `build/.venv_optimum`. ## [1.0.0] — 2026-04-22 ### Added - Initial release of the DeepStream Bring Your Own Vision Model (BYOVM) skill - End-to-end pipeline: HuggingFace / NGC model → ONNX → TensorRT engine → DeepStream → benchmark report - Four orchestrated sub-skills under `skills/`: - `hf-model-acquire` — model download and format routing (ONNX vs SafeTensors) - `trt-engine-build` — dynamic TRT engine build + `trtexec` benchmarks - `ds-integration` — custom `nvinfer` parser, single-stream + multi-stream DS runs - `benchmark-report` — 5-chart Markdown → HTML → PDF report - Runtime scripts under `scripts/`: - `model/` — HF/NGC list + download helpers, ONNX inspection, SafeTensors → ONNX export, scoped cleanup - `engine/` — `trtexec` benchmark helper - `deepstream/` — single-stream, sweep, KITTI dump, frame extraction helpers - `report/` — chart generation, Mermaid → PNG, Markdown → HTML → PDF - Installer (`install.sh`) with validated `--target` and dry-run mode - Declared `permissions:` block in `SKILL.md` frontmatter: tool allowlist, MCP scope, network egress allowlist (`huggingface.co`, `api.ngc.nvidia.com`, `api-inference.huggingface.co`), and filesystem read/write scoping - Test suite (`tests/test_hardened_scripts.py`) covering input validation for all hardened shell scripts and the PDF image-embedding helper (24 tests) ### Security - All shell scripts validate inputs against `^[A-Za-z0-9._/-]+$` or tighter before touching filesystem or network - `curl` invocations pinned to HTTPS + TLSv1.2 with bounded timeouts; optional `$HF_TOKEN` honored for gated HuggingFace repos - `install.sh` rejects `--target` values that are empty, `/`, contain `..`, or don't exist; destructive `rm -rf` is scoped to paths under `$TARGET` - `scripts/report/md-to-html-pdf.py` base64-inlines images before rendering; `wkhtmltopdf` runs without `--enable-local-file-access` - `scripts/report/render-mermaid-for-pdf.py` refuses user-supplied Puppeteer configs; always uses the vetted `mermaid-puppeteer.json` shipped with the skill ### Known limitations - Nested sub-skills under `skills/` surface a low-severity schema warning from some scanners; kept in place because the agent file preloads them by name and the installer symlinks them into `.claude/skills/` at the target - Engine build time depends on GPU, ONNX complexity, and requested batch size; the skill retries on OOM by halving batch size but gives up after reaching 1 -
install.ps1 5.4 KB · in bundle
-
install.sh 6.3 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2025 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. # deepstream-import-vision-model — Install script # Installs the skills and runtime scripts into a target project. # Supports Claude Code (.claude/), Codex (.codex/), and Cursor (.cursor/) out of the box. # # Usage: # bash install.sh --target <project-path> [--dry-run] [--no-cursor] # # Examples: # bash install.sh --target ~/work/my-project --dry-run # bash install.sh --target ~/work/my-project # bash install.sh --target ~/work/my-project --no-cursor set -euo pipefail SKILL_DIR="$(cd "$(dirname "$0")" && pwd)" TARGET="" DRY_RUN=false NO_CURSOR=false usage() { local rc="${1:-0}" cat <<EOF Usage: $0 --target <project-path> [--dry-run] [--no-cursor] --target <path> Project directory to install into (required) --dry-run Show what would be done without making any changes --no-cursor Skip Cursor (.cursor/skills/) installation -h, --help Show this help Example: bash install.sh --target ~/work/my-deepstream-project EOF exit "$rc" } while [[ $# -gt 0 ]]; do case "$1" in --target) if [[ $# -lt 2 || -z "${2:-}" || "$2" == -* ]]; then echo "Error: --target requires a path argument" >&2 usage 1 fi TARGET="$2" shift 2 ;; --dry-run) DRY_RUN=true; shift ;; --no-cursor) NO_CURSOR=true; shift ;; -h|--help) usage 0 ;; *) echo "Unknown option: $1" >&2; usage 1 ;; esac done # --- TARGET validation (hardened) ---------------------------------------------- if [[ -z "$TARGET" ]]; then echo "Error: --target is required" >&2 usage 1 fi case "$TARGET" in ""|"/"|*..*) echo "Error: invalid --target value: $TARGET" >&2 exit 1 ;; esac if [[ ! -d "$TARGET" ]]; then echo "Error: target directory not found: $TARGET" >&2 exit 1 fi TARGET="$(cd "$TARGET" && pwd -P)" if [[ "$TARGET" == "/" ]] || [[ ${#TARGET} -lt 3 ]]; then echo "Error: refusing to install into '$TARGET' (path too short / is root)" >&2 exit 1 fi # ------------------------------------------------------------------------------- do_copy() { local src="$1" local dest="$2" if $DRY_RUN; then echo " [dry-run] cp -r $src -> $dest" else mkdir -p "$(dirname "$dest")" cp -r "$src" "$dest" echo " Copied: $(basename "$dest")" fi } # Scoped cleanup helper: only removes a path that is a subdirectory of $TARGET. safe_rm_under_target() { local path="$1" local resolved resolved="$(cd "$(dirname "$path")" 2>/dev/null && pwd -P)/$(basename "$path")" || return 1 case "$resolved" in "$TARGET"/*) ;; *) echo " Refusing to remove $resolved (not under $TARGET)" >&2; return 1 ;; esac if $DRY_RUN; then echo " [dry-run] rm -rf $resolved" else echo " Removing: $resolved" rm -rf "$resolved" fi } # Install the WHOLE self-contained skill (SKILL.md + references/ + scripts/ + setup.sh + .gitattributes) # into a skills directory, so it runs through Docker as one mounted tree (-v <root>:/work). Works for # .claude/skills/, .codex/skills/, and .cursor/skills/. install_skills_to_dir() { local skills_dir="$1" local skill_dest="$skills_dir/deepstream-import-vision-model" if [[ -d "$skill_dest" ]]; then local dest_real dest_real="$(cd "$skill_dest" && pwd -P)" if [[ "$dest_real" == "$SKILL_DIR" ]]; then echo " Already installed at $skill_dest; source and destination are identical" return fi safe_rm_under_target "$skill_dest" fi if $DRY_RUN; then echo " [dry-run] cp -r $SKILL_DIR -> $skill_dest (whole skill; minus __pycache__/*.pyc/built parser .so)" return fi mkdir -p "$(dirname "$skill_dest")" cp -r "$SKILL_DIR" "$skill_dest" # strip machine-local build artifacts (the venv + parser .so are rebuilt in-container by setup.sh) find "$skill_dest" -type d -name '__pycache__' -prune -exec rm -rf {} + 2>/dev/null || true find "$skill_dest" \( -name '*.pyc' -o -name '*.so' -o -name '*.o' \) -delete 2>/dev/null || true echo " Copied self-contained skill -> $skill_dest" } echo "=== deepstream-import-vision-model Install ===" echo "Skill dir: $SKILL_DIR" echo "Target: $TARGET" echo "Cursor: $($NO_CURSOR && echo "disabled (--no-cursor)" || echo "enabled")" echo "" # Step 1: Claude Code and Codex skills echo "Claude Code skills -> $TARGET/.claude/skills/" install_skills_to_dir "$TARGET/.claude/skills" echo "" echo "Codex skills -> $TARGET/.codex/skills/" install_skills_to_dir "$TARGET/.codex/skills" echo "" # Step 2: Cursor — skills only (Cursor does not support agents) if ! $NO_CURSOR; then echo "Cursor skills -> $TARGET/.cursor/skills/" install_skills_to_dir "$TARGET/.cursor/skills" echo "" fi echo "" echo "=== Done ===" echo "" echo "Next — bootstrap the environment IN the container (nothing installs on the host):" echo " docker run --rm -it --gpus all --shm-size=16g -v \"\$PWD\":/work -w /work \\" echo " --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \\" echo " .claude/skills/deepstream-import-vision-model/setup.sh" echo " (PowerShell: use -v \"\${PWD}:/work\" · see references/windows.md)" echo "" echo "Claude Code — invoke the skill:" echo " Use deepstream-import-vision-model to run this model: https://huggingface.co/onnx-community/yolov8n" if ! $NO_CURSOR; then echo "" echo "Cursor — invoke the skill:" echo " @deepstream-import-vision-model run this model: https://huggingface.co/onnx-community/yolov8n" fi -
README.md 4 KB
# DeepStream Import Vision Model Automated end-to-end pipeline: HuggingFace model → TensorRT engine → DeepStream multi-stream benchmark → PDF report. ## Overview This self-contained skill uses four phase-specific reference documents. Together they automate the full model bringup workflow for NVIDIA DeepStream, from downloading a model on HuggingFace or NVIDIA NGC to a publication-ready benchmark report. Supported input formats: ONNX (direct), SafeTensors (auto-exported via `torch.onnx.export`). **Current scope:** object detection models only. Classification, segmentation, pose estimation, and other vision tasks are not yet supported — the pipeline fails fast if a non-detection architecture is detected in `config.json`. ## Prerequisites **Host: only Docker + the NVIDIA driver.** Everything else runs **inside the DeepStream container** — DeepStream, TensorRT/`trtexec`, the Python export venv (`torch`/`onnx`/`onnxruntime`), `wkhtmltopdf`, `deepstream-app`/`gst-launch-1.0` — and is bootstrapped by `setup.sh`. Nothing is installed on the host. Runs identically on Linux and **Windows** (Docker Desktop + WSL2 backend, required for GPU). - **Docker** — Docker Desktop with the WSL2 backend on Windows; Docker Engine + NVIDIA Container Toolkit on Linux. - **NVIDIA GPU + driver** (on Windows, the WSL2 GPU driver — no host CUDA/TensorRT/DeepStream needed). - `docker pull nvcr.io/nvidia/deepstream:9.1-triton-multiarch`, then run `setup.sh` through the container. See **[references/windows.md](references/windows.md)** for the cross-platform run model and the per-shell `docker run` mount token. ## Installation ```bash bash <path-to-deepstream-import-vision-model>/install.sh --target <your-project-path> ``` Preview what will be installed first with `--dry-run`: ```bash bash <path-to-deepstream-import-vision-model>/install.sh --target <your-project-path> --dry-run ``` Where `<path-to-deepstream-import-vision-model>` is the location of this skill in your repo, e.g.: - In **team-mind-hub**: `team-skills/deepstream-sdk/deepstream-import-vision-model` - In **ds-copilot**: `team-skills/deepstream-sdk/deepstream-import-vision-model` (same path) The script copies the complete skill into the target project for Claude Code, Codex, and Cursor. Re-running it safely refreshes an existing installation. ## Usage **Claude Code:** ```text Use deepstream-import-vision-model to run this model: https://huggingface.co/onnx-community/yolov8n ``` **Codex:** ```text Use $deepstream-import-vision-model to deploy and benchmark https://huggingface.co/onnx-community/yolov8n ``` **Cursor:** ```text @deepstream-import-vision-model run this model: https://huggingface.co/onnx-community/yolov8n ``` The skill runs the full pipeline autonomously — no manual steps required. ## Pipeline Steps | Step | Phase reference | Action | |------|-----------|--------| | 1–3 | `references/model-acquire.md` | Browse HF repo, download ONNX or export SafeTensors | | 4–5 | `references/engine-build.md` | Build dynamic TRT engine, run trtexec benchmarks | | 6–7 | `references/pipeline-run.md` | Custom bbox parser, DeepStream single + multi-stream | | 8 | `references/report-generation.md` | 5 charts, HTML report, PDF | ## Output Structure Per-model outputs are written to `models/<model_name>/` in your project: ```text models/<model_name>/ model/ ONNX file(s) parser/ Custom nvinfer bbox parser (.cpp, .so) config/ nvinfer config, DS app config, labels.txt scripts/ Model-specific run helpers benchmarks/ TRT engines, trtexec logs reports/ benchmark_report.md / .html / .pdf + charts/ samples/ Output videos, test frames, KITTI detections ``` ## Files in this package ```text deepstream-import-vision-model/ ├── SKILL.md Top-level skill definition ├── README.md This file ├── references/ Phase-specific runbooks ├── scripts/ Utility scripts by pipeline phase └── tests/ Installer and script regression tests ``` -
setup.sh 3.6 KB
#!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Apache-2.0. # # One-command environment bootstrap for deepstream-import-vision-model on a FRESH machine. # Creates the shared venv (build/.venv_optimum), installs the ONNX-export + report Python deps, # and installs wkhtmltopdf (for the PDF report) — ALL INSIDE the container. Idempotent. # # Nothing runs on the host: the host only needs Docker + the NVIDIA driver. Run this INSIDE the # DeepStream container, from the working root (where models/, reports/, build/ will live) — so # torch/CUDA/TensorRT and the compiled parser all match the runtime: # # # Linux / WSL2 bash: # docker run --rm -it --gpus all --shm-size=16g -v "$PWD":/work -w /work \ # --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \ # .claude/skills/deepstream-import-vision-model/setup.sh # # PowerShell: use -v "${PWD}:/work" · cmd: -v "%cd%:/work" (see references/windows.md) set -euo pipefail SK=".claude/skills/deepstream-import-vision-model" VENV="build/.venv_optimum" PY="$VENV/bin/python" if [ ! -d "$SK" ]; then echo "[setup] ERROR: run from the working root (the dir that contains $SK)." >&2 exit 1 fi # 1) virtualenv (the container python lacks ensurepip, so bootstrap virtualenv via pip) if [ ! -x "$PY" ]; then echo "[setup] creating venv at $VENV" python3 -m pip install --quiet --user virtualenv 2>/dev/null || python3 -m pip install --quiet virtualenv python3 -m virtualenv "$VENV" else echo "[setup] venv exists: $VENV" fi # 2) Python dependencies (ONNX export + report) echo "[setup] installing Python deps from $SK/scripts/requirements.txt (this can take several minutes)" "$PY" -m pip install --quiet --upgrade pip "$PY" -m pip install -r "$SK/scripts/requirements.txt" # 3) wkhtmltopdf for the PDF report — installed IN the container (not a host dependency). # Self-contained Qt-WebKit renderer; no browser/Chromium needed. if command -v wkhtmltopdf >/dev/null 2>&1; then echo "[setup] wkhtmltopdf already present" else echo "[setup] installing wkhtmltopdf (apt, in-container)" if [ "$(id -u)" = "0" ]; then APT=""; else APT="sudo"; fi $APT apt-get update -qq && $APT apt-get install -y -qq wkhtmltopdf \ || echo "[setup] WARN: wkhtmltopdf install failed — the HTML report still works; PDF step will skip" fi # 4) verify echo "[setup] verifying…" "$PY" - <<'PYV' import importlib.util, sys mods = ["torch","torchvision","transformers","onnx","onnxruntime","onnxscript", "huggingface_hub","matplotlib","numpy","markdown","reportlab"] missing = [m for m in mods if importlib.util.find_spec(m) is None] import torch print(f" torch {torch.__version__} cuda_available={torch.cuda.is_available()}") if missing: print(" MISSING:", missing); sys.exit(1) print(" all required packages present") PYV command -v wkhtmltopdf >/dev/null 2>&1 && echo " wkhtmltopdf: $(command -v wkhtmltopdf)" || echo " wkhtmltopdf: (absent — PDF step will skip)" command -v /usr/src/tensorrt/bin/trtexec >/dev/null 2>&1 && echo " trtexec: /usr/src/tensorrt/bin/trtexec" || echo " trtexec: (check TensorRT in image)" echo echo "[setup] DONE. Next: run preflight, then the phases via the container —" echo " docker run --rm --gpus all -v \"\$PWD\":/work -w /work --entrypoint bash \\" echo " nvcr.io/nvidia/deepstream:9.1-triton-multiarch $SK/scripts/preflight.sh" echo " (see $SK/SKILL.md + $SK/references/windows.md for the per-shell mount token)" -
skill-card.md 4.5 KB
## Description: <br> Use this skill to bring a supported object-detection vision model from HuggingFace or NVIDIA NGC into an NVIDIA DeepStream pipeline with end-to-end automation: ONNX download, SafeTensors export, TRT engine build, custom nvinfer bbox parser, multi-stream benchmark, and PDF report. <br> This skill is ready for commercial/non-commercial use. <br> ## Owner NVIDIA <br> ### License/Terms of Use: <br> CC-BY-4.0 AND Apache-2.0 <br> ## Use Case: <br> Developers and engineers use this skill to import supported object-detection vision models from HuggingFace or NVIDIA NGC into NVIDIA DeepStream inference pipelines, automating the full workflow from model acquisition through TensorRT engine build, multi-stream benchmarking, and PDF report generation. <br> ### Deployment Geography for Use: <br> Global <br> ## Requirements / Dependencies: <br> **Requires API Key or External Credential:** [Not Specified] <br> **Credential Type(s):** [None identified] <br> Do not include secrets in prompts/logs/output; use least-privilege credentials; rotate keys as appropriate. <br> ## Known Risks and Mitigations: <br> Risk: Review before execution as proposals could introduce incorrect or misleading guidance into skills. <br> Mitigation: Review and scan skill before deployment. <br> ## Reference(s): <br> - [Model Acquire](references/model-acquire.md) <br> - [Engine Build](references/engine-build.md) <br> - [Pipeline Run](references/pipeline-run.md) <br> - [Report Generation](references/report-generation.md) <br> - [Windows Support](references/windows.md) <br> ## Skill Output: <br> **Output Type(s):** [Shell commands, Code, Files, Analysis] <br> **Output Format:** [TensorRT engines, ONNX models, C++ parser source, nvinfer configs, benchmark logs, PDF/HTML/Markdown reports] <br> **Output Parameters:** [1D] <br> **Other Properties Related to Output:** [Outputs organized in a mandatory directory structure under models/{model_name}/] <br> ## Evaluation Agents Used: <br> - Claude Code (`aws/anthropic/bedrock-claude-opus-4-8`) <br> - Codex (`openai/openai/gpt-5.5`) <br> ## Evaluation Tasks: <br> 13 evaluation tasks (13 positive), each run in isolated sandbox pods. <br> ## Evaluation Metrics Used: <br> Reported benchmark dimensions: <br> - Security: Checks whether the skill is safe to use — detects unsafe operations, secret leakage, and unauthorized access. <br> - Correctness: Checks whether the final answer is correct against the reference answer. <br> - Discoverability: Checks whether the right skill was found and executed when needed. <br> - Effectiveness: Checks whether the skill helped complete the user's goal (goal completion and expected workflow adherence). <br> - Efficiency: Checks routing quality, workspace-aware skill reads, and productive tool use. <br> Underlying evaluation signals used in this run: <br> - `security`: Detects unsafe operations, secret leakage, and unauthorized access. <br> - `skill_execution`: Verifies whether the expected skill was found and executed. <br> - `skill_efficiency`: Evaluates routing quality, workspace-aware skill reads, and productive tool use. <br> - `accuracy`: Measures final-answer correctness against the reference answer. <br> - `goal_accuracy`: Measures whether the user's goal was achieved. <br> - `behavior_check`: Verifies whether the expected workflow behavior was followed. <br> ## Evaluation Results: <br> | Measure | Claude Code (Baseline → Skill Uplift) | Codex (Baseline → Skill Uplift) | |---|---:|---:| | Overall | 44% → 72% (+27 points) | 46% → 61% (+15 points) | | Security | 58% → 54% (-4 points) | 35% → 42% (+8 points) | | Correctness | 42% → 86% (+45 points) | 65% → 72% (+8 points) | | Discoverability | 51% → 88% (+37 points) | 50% → 72% (+22 points) | | Effectiveness | 24% → 43% (+19 points) | 28% → 34% (+6 points) | | Efficiency | 48% → 86% (+38 points) | 54% → 84% (+30 points) | ## Skill Version(s): <br> 1.5.2 (source: frontmatter) <br> ## Ethical Considerations: <br> NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal team to ensure this skill meets requirements for the relevant industry and use case and addresses unforeseen product misuse. <br> (For Release on NVIDIA Platforms Only) <br> Please report quality, risk, security vulnerabilities or NVIDIA AI Concerns [here](https://app.intigriti.com/programs/nvidia/nvidiavdp/detail). <br> -
SKILL.md 14.2 KB
--- name: deepstream-import-vision-model description: > Use this skill to bring a supported object-detection vision model from HuggingFace or NVIDIA NGC into an NVIDIA DeepStream pipeline with end-to-end automation: ONNX download, SafeTensors export, TRT engine build, custom nvinfer bbox parser, multi-stream benchmark, and PDF report. Object detection models only. license: CC-BY-4.0 AND Apache-2.0 metadata: author: "Tushar Khinvasara <tkhinvasara@nvidia.com>" owner: "Tushar Khinvasara <tkhinvasara@nvidia.com>" service: "deepstream" version: "1.5.2" reviewed: "2026-08-04" team: deepstream-sdk tags: - deepstream - tensorrt - object-detection - import-vision-model languages: - bash - python - cpp domain: computer-vision --- # DeepStream Import Vision Model When this skill is active, **read the relevant reference document before starting each phase**. Do not rely on memory — reference documents contain exact script paths, bash variable conventions, log filename contracts, and critical parsing rules. **Current scope:** Object detection models only. Fail fast on classification, segmentation, or other architectures detected in `config.json`. ## Model choice — always offer two options Before preflight, browsing, downloads, or file creation, present exactly these two choices. Do not start with only an open-ended model-source prompt. If the user's request already clearly selects a model, confirm the matching choice instead of asking redundantly. ### 1. Default model (recommended) Use the validated Hugging Face RT-DETR model: ```yaml model_id: PekingU/rtdetr_r50vd source: huggingface task: object-detection precision_preference: fp16 ``` ### 2. Custom object-detection model Ask for one supported source: - Hugging Face model ID (`organization/model`) or full model URL. - NVIDIA NGC catalog model URL including its version. Explain that the skill currently rejects classification, segmentation, and other non-detection architectures after inspecting `config.json`. Do not invent or silently substitute a model when the custom source is missing or unsupported. For a dry run, present the same two choices and simulate discovery, build, benchmark, and report stages without browsing, downloading, launching Docker, writing files, or starting processes. ## Pipeline Overview | Step | Phase | Reference | What it does | |------|-------|-----------|--------------| | 1–3 | Model Acquire | [references/model-acquire.md](references/model-acquire.md) | Browse HF/NGC, detect format, download ONNX or export SafeTensors | | 4–5 | Engine Build | [references/engine-build.md](references/engine-build.md) | Build dynamic TRT engine, run trtexec BS=1 and BS=MAX_BS | | 6–7 | DS Pipeline | [references/pipeline-run.md](references/pipeline-run.md) | Custom bbox parser, nvinfer config, single-stream + multi-stream benchmarks | | 8 | Report | [references/report-generation.md](references/report-generation.md) | 5 charts, HTML, PDF benchmark report | Run the full pipeline autonomously without pausing for confirmation at each step. ## Runs entirely through Docker (no host packages) **Every step runs INSIDE the DeepStream container.** The host needs only **Docker + the NVIDIA driver** — no host python/venv/torch/trtexec/make/wkhtmltopdf. This works identically on Linux and **Windows** (Docker Desktop + WSL2 backend, required for `--gpus`). The per-shell bind-mount token is the only OS difference — `-v "$PWD":/work` (bash), `-v "${PWD}:/work"` (PowerShell), `-v "%cd%:/work"` (cmd); full guide in [references/windows.md](references/windows.md). All venv/ONNX/ engine/parser/config/report artifacts live under the mounted working root and persist between the ephemeral `--rm` containers. ## Pre-flight — bootstrap + verify (through the container) **1. One-time bootstrap** — builds `build/.venv_optimum` (torch/onnx/onnxruntime/report deps; the venv name is historical, optimum is no longer used) + installs `wkhtmltopdf`, all in-container. From the working root: ```bash docker run --rm -it --gpus all --shm-size=16g -v "$PWD":/work -w /work \ --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \ .claude/skills/deepstream-import-vision-model/setup.sh ``` **2. Preflight** — GPU + venv + trtexec, run THROUGH the container (container-mode auto-detects): ```bash docker run --rm --gpus all -v "$PWD":/work -w /work \ --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \ .claude/skills/deepstream-import-vision-model/scripts/preflight.sh # proceed only on PASS ``` **Every subsequent phase runs the same way** — issue the model's commands via `docker run … --entrypoint bash … -lc '<commands>'` (or the `.claude/skills/deepstream-import-vision-model/scripts/dsrun.sh` wrapper: `bash .claude/skills/deepstream-import-vision-model/scripts/dsrun.sh '<in-container command>'`), using `PY=build/.venv_optimum/bin/python` and `trtexec` at `/usr/src/tensorrt/bin/trtexec` inside the container. `deepstream-app`, `gst-launch-1.0`, and `/opt/nvidia/deepstream/…` sample paths all exist **in** the image. TensorRT build+runtime share one image, so there is **no version skew** (the concern the old "build on the host" rule tried to avoid — see [references/engine-build.md](references/engine-build.md)). `sample_720p.mp4` ships in the image; set `DS_VIDEO` only to override. ## Mandatory Output Structure Create once `MODEL_NAME` is known (Step 1). Never dump files flat. ``` models/{model_name}/ model/ <- ONNX file(s) parser/ <- .cpp, Makefile, .so config/ <- nvinfer config, ds-app config, labels.txt scripts/ <- run helper scripts benchmarks/ engines/ <- _dynamic_b{MAX_BS}.engine, timing.cache, build logs b1/ <- trtexec BS=1 log b{MAX_BS}/ <- trtexec BS=MAX_BS log ds/ <- DS benchmark logs reports/ <- benchmark_report.md, .html, .pdf, benchmark_data.json charts/ <- chart_*.png (5 charts) samples/ <- output .mp4 or .ogv (theoraenc fallback), test frames kitti_output/ <- KITTI detection .txt files ``` ```bash mkdir -p models/$MODEL_NAME/{model,parser,config,scripts,benchmarks/engines,benchmarks/ds,reports/charts,samples/kitti_output} ``` ## Critical Rules 1. **Engine naming** — always `{model}_dynamic_b{MAX_BS}.engine`. Never bare `model_dynamic.engine`. 2. **batch_size == num_streams** — in DS runs, `batch-size` and stream count are always equal. 3. **Log filenames are fixed** — `trtexec_b1.log`, `trtexec_b${MAX_BS}.log`, `ds_s${N}_run1.log`, `ds_s${N}_run2.log`. No timestamps. Report generation reads exact paths. 4. **Parser zero-init** — always `NvDsInferObjectDetectionInfo obj = {};`. Required for DS 9.1 OBB support; bare `obj;` leaves `rotation_angle` uninitialized, causing tilted bounding boxes. 5. **KITTI validation gate** — do NOT proceed to Step 7 if KITTI frame count is zero or detection rate < 90%. 6. **Shared venv** — `build/.venv_optimum` reused across all models. Never create per-model venvs. 7. **trtexec `--noDataTransfers`** — GPU-only compute matches DeepStream's GPU-to-GPU data flow. 8. **Report HTML+PDF** — always use `.claude/skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py`. Never write a custom HTML generator or call `wkhtmltopdf` directly. 9. **Object detection only** — reject non-detection architectures from `config.json` before building anything. 10. **Encoder fallback (MANDATORY)** — `x264enc` and `openh264enc` are **prohibited**. On NVENC-unavailable systems, use `theoraenc + oggmux` (LGPL; ships in gst-plugins-base; output is `.ogv`). If `theoraenc`/`oggmux` are absent, skip video creation (`DS_SINGLE_STREAM_MODE=skipped`). Report which mode was used: `nvv4l2h264enc` / `theoraenc-fallback` / `skipped`. 11. **Video source (MANDATORY)** — default is always `sample_720p.mp4` (1280×720). Never autonomously substitute `sample_1080p_h264.mp4` or any other file. Only use a different video when the user explicitly provides a path (via `DS_VIDEO` env var or script argument). ## Examples **Default model, end to end.** Bootstrap once, then run the full pipeline: ```bash docker run --rm -it --gpus all --shm-size=16g -v "$PWD":/work -w /work \ --entrypoint bash nvcr.io/nvidia/deepstream:9.1-triton-multiarch \ .claude/skills/deepstream-import-vision-model/setup.sh # then: "Use deepstream-import-vision-model to run PekingU/rtdetr_r50vd" ``` **SafeTensors model with no published ONNX.** Step 2b exports it first; the wrapper reports which backend produced the graph and fails loudly if the batch dimension was baked in: ```bash bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \ models/$MODEL_NAME/hf_model models/$MODEL_NAME/onnx_export/ # [export] backend=dynamo # [export] dynamo produced a static batch dimension; trying the next backend # [export] backend=legacy-torchscript # [export] pixel_values shape=['batch', 3, 640, 640] ``` **Pin a Hub revision** for a reproducible build — any exporter flag passes straight through: ```bash bash .claude/skills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.sh \ PekingU/rtdetr_r50vd models/rtdetr/onnx_export --revision <commit-sha> --opset 18 ``` ## Pipeline Timing Wrap every step: ```bash STEP_START=$(date +%s.%N) # ... step commands ... STEP_END=$(date +%s.%N) STEP_DURATION=$(python3 -c "print(round($STEP_END - $STEP_START, 2))") # bc is not in the container; python3 always is echo "[Step N] completed in ${STEP_DURATION}s" ``` Track `PIPELINE_START` (before Step 1) and `PIPELINE_END` (after Step 8). Report all durations in the benchmark report. ## Report Output (MANDATORY — all 3 formats) 1. `benchmark_report.md` — markdown source (12 mandatory sections) 2. `benchmark_report.html` — styled HTML (charts base64-inlined, no local file access) 3. `benchmark_report_{model_name}.pdf` — via `md-to-html-pdf.py`; verify charts are embedded by counting `data:image/png` occurrences in the HTML output: `grep -o 'data:image/png' benchmark_report.html | wc -l` should equal 5 Run charts and report scripts with the shared venv active: `source build/.venv_optimum/bin/activate`. ## Reference Documents **IMPORTANT**: Read the relevant reference before starting each phase. Do NOT generate code from memory. | Document | Use When | |----------|----------| | [references/model-acquire.md](references/model-acquire.md) | Steps 1–3: HF/NGC URL parsing, format detection, ONNX download, SafeTensors export, label extraction | | [references/engine-build.md](references/engine-build.md) | Steps 4–5: trtexec engine build, benchmarks, PEAK_GPU_STREAMS derivation, iterative scaling | | [references/pipeline-run.md](references/pipeline-run.md) | Steps 6–7: custom bbox parser, nvinfer config, single-stream validation, KITTI dump, multi-stream benchmark | | [references/report-generation.md](references/report-generation.md) | Step 8: benchmark_data.json, 5 charts, 12-section markdown report, HTML + PDF | ## Scripts Installed into `.claude/skills/deepstream-import-vision-model/scripts/` by `install.sh`. | Script | Phase | Purpose | |--------|-------|---------| | `model/hf-list-files.sh` | 1–3 | List HuggingFace repo files | | `model/hf-download-config.sh` | 1–3 | Download config.json from HF | | `model/ngc-list-files.sh` | 1–3 | List NGC model files | | `model/ngc-download.sh` | 1–3 | Download NGC model archive | | `model/safetensors-to-onnx.sh` | 1–3 | Export SafeTensors → ONNX via `torch.onnx.export` (wrapper) | | `model/safetensors_to_onnx.py` | 1–3 | The exporter — dynamo backend, TorchScript fallback, verifies dynamic batch | | `model/inspect-onnx.py` | 1–5 | Inspect ONNX input/output shapes | | `model/make-static-batch-onnx.py` | 4–5 | Bake batch dim into ONNX | | `model/cleanup.sh` | Any | Remove staging dirs, preserve shared venv | | `engine/benchmark-trtexec.sh` | 4–5 | Run trtexec with standard flags | | `deepstream/ds-single-stream.sh` | 6–7 | Single-stream visual validation (NVENC primary; theoraenc+oggmux fallback; skip if neither) | | `deepstream/ds-sweep.sh` | 6–7 | 2-phase batch size sweep | | `deepstream/benchmark-ds.sh` | 6–7 | Fixed-stream DS benchmark | | `deepstream/ds-kitti-dump.sh` | 6–7 | KITTI detection dump via deepstream-app | | `deepstream/ds-perf-run.sh` | 7 | Step 7c two-run benchmark — wraps `deepstream-app` with `enable-perf-measurement=1`, writes fixed-name log for the report parser | | `deepstream/extract-frame.sh` | 6–7 | Extract sample frames from output video (`.mp4` NVENC path or `.ogv` theoraenc fallback) | | `report/generate-benchmark-charts.py` | 8 | Generate 5 benchmark PNG charts | | `report/md-to-html-pdf.py` | 8 | Markdown → styled HTML → PDF (canonical benchmark report path) | | `report/md-to-pdf.sh` | Any | Markdown → PDF via pandoc/pdflatex — for design docs and references only, NOT for benchmark reports (use md-to-html-pdf.py for those) | | `report/report-style.css` | 8 | CSS for HTML report | | `report/render-mermaid-for-pdf.py` | 8 | Mermaid diagram → PNG | | `report/mermaid-puppeteer.json` | 8 | Vetted Puppeteer config for Mermaid (sandboxed; non-root) | | `report/mermaid-puppeteer-root.json` | 8 | Vetted Puppeteer config for Mermaid (used when running as root) | ## Quick Error Reference | Error | Fix | |-------|-----| | Tilted/diagonal bounding boxes | Parser struct not zero-initialized — use `NvDsInferObjectDetectionInfo obj = {};` | | Zero KITTI files | `gie-kitti-output-dir` not read by nvinfer — use `ds-kitti-dump.sh` (wraps `deepstream-app`) | | Engine rebuilds every DS run | `model-engine-file` path wrong — check relative path from `config/` dir | | `setDimensions` negative dims | Add `infer-dims=3;H;W` to nvinfer config for dynamic ONNX models | | `--memPoolSize` workspace 0.03 MiB | Use `M` suffix not `MiB` — e.g. `--memPoolSize=workspace:32768M` | | ForeignNode build failure (DETR) | Run `onnxsim` — see references/engine-build.md. Not reproduced on TRT 10.16 with either export backend | | ONNX has a static batch dim | Both export backends specialized it — see the gotchas in references/model-acquire.md | | Zero detections | Wrong `net-scale-factor` — check model family table in references/pipeline-run.md | | `No module named 'pyservicemaker'` | Install into venv: `pip install /opt/nvidia/deepstream/.../pyservicemaker*.whl` | -
skill.oms.sig 16.1 KB · in bundle
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.