
Deepstream Import Vision Model
- 3 installs
- 7 repo stars
- Updated August 2, 2026
- practicalswan/agent-skills
deepstream-import-vision-model is a Claude Code skill in the AI & Agent Building category.
About
Provides steps to bring a vision model into a DeepStream pipeline, including export, TensorRT engine build, and benchmarking. A developer uses it when integrating a Hugging Face or NGC vision model into DeepStream.
- Export and TensorRT build for imported models
- Shared Python venv and GPU/driver version checks
Deepstream Import Vision Model by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,661 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/practicalswan/agent-skills --skill deepstream-import-vision-modelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 7 |
| Last updated | August 2, 2026 |
| Repository | practicalswan/agent-skills ↗ |
How do I helps with ai & agent building tasks.?
Imports vision models from Hugging Face or NVIDIA NGC into DeepStream pipelines with export, TensorRT build, and benchmark steps.
Who is it for?
A solo builder working on ai & agent building tasks who needs structured help with deepstream import vision model.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks., or when deepstream-import-vision-model is a claude code skill in the ai & agent building category.
What you get
Structured output aligned to deepstream-import-vision-model: deepstream-import-vision-model, AI & Agent Building.
Files
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.
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.
Pre-flight Checks
Run before starting:
# 1. GPU and drivers
nvidia-smi
# 2. TensorRT version match (must match between builder and DS runtime)
trtexec 2>&1 | head -3
dpkg -l | grep libnvinfer-bin
# 3. Shared Python venv — create once, reuse across all models
mkdir -p build
VENV=build/.venv_optimum
if [ ! -x "$VENV/bin/python3" ]; then
python3 -m venv "$VENV"
"$VENV/bin/pip" install --upgrade pip -q
"$VENV/bin/pip" install "optimum[exporters]>=1.20,<2.0" "torch<2.12" \
transformers onnxruntime matplotlib numpy markdown -q
fi
# 4. System tools
which wkhtmltopdf || apt-get install -y wkhtmltopdf
which mediainfo || apt-get install -y mediainfo
which deepstream-app # required for KITTI dump (Step 6g) and benchmark perf-measurement (Step 7c); shipped with DeepStream SDK
# 5. Sample video — only check default path when user has not provided a custom DS_VIDEO
if [ -z "$DS_VIDEO" ]; then
[ -f /opt/nvidia/deepstream/deepstream/samples/streams/sample_720p.mp4 ] || \
echo "WARNING: sample_720p.mp4 not found. Install DeepStream samples or set DS_VIDEO=/path/to/your.mp4"
fiMandatory 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 filesmkdir -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.0 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 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).
Pipeline Timing
Wrap every step:
STEP_START=$(date +%s.%N)
# ... step commands ...
STEP_END=$(date +%s.%N)
STEP_DURATION=$(echo "$STEP_END - $STEP_START" | bc)
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 | 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
Located in scripts/.
| 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 optimum-cli |
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) | Use dynamo export path or run onnxsim — see references/engine-build.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 |
<!-- Signing refresh marker. -->
Anti-Patterns
- Assuming every vision checkpoint can drop into DeepStream unchanged: Export format, parser behavior, and label mapping often need explicit work.
- Skipping intermediate validation between model export, TensorRT engine build, and pipeline integration: It makes failures much harder to localize.
- Using a single happy-path stream as the only benchmark: Multi-stream behavior and parser correctness can fail later.
Verification Protocol
Before claiming "skill applied successfully":
1. Pass/fail: The workflow identifies the source model format, conversion path, parser needs, and target DeepStream integration point before build steps begin. 2. Pass/fail: Each stage (export, engine build, pipeline integration, benchmark) has a distinct validation checkpoint instead of one end-only test. 3. Pass/fail: The answer preserves label, bbox, and multi-stream assumptions so production behavior is not guessed from a demo run. 4. Pressure-test scenario: Apply the workflow to a Hugging Face detector that exports to ONNX cleanly but mislabels boxes once inside DeepStream. 5. Success metric: The user gets a model-import path with stage-by-stage evidence, not just a final engine artifact.
<!-- PORTABILITY:START -->
Cross-Client Portability
This skill is written to stay usable across GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- GitHub Copilot: keep the folder in a Copilot-visible skill or plugin path, or wrap the workflow as project instructions if the host does not support portable skill folders directly.
- Claude Code: keep the folder in a local skills directory or a compatible plugin or marketplace source.
- Codex: install or sync the folder into
$CODEX_HOME/skills/<skill-name>and restart Codex after major changes. - Gemini CLI: this repository generates a project command named
/skills:deepstream-import-vision-modelfrom this skill. Rebuild commands withpython scripts/export-gemini-skill.py deepstream-import-vision-modeland then run/commands reloadinside Gemini CLI.
<!-- PORTABILITY:END -->
<!-- MCP:START -->
MCP Availability And Fallback
Preferred MCP Server: None required
- Fallback prompt: "Use the deepstream-import-vision-model skill without MCP. Rely on the local
SKILL.md, bundled references or scripts, and manual verification. Show the exact commands, evidence, and final checks you used before concluding." - If the current host does not expose a matching server, use the bundled references, scripts, native toolchain, and manual workflow already described in this skill.
- Treat direct local verification, rendered output, logs, tests, or screenshots as the fallback evidence path before completion.
<!-- MCP:END -->
Related Skills
- development-workflow: Use it when the import project also needs a scoped implementation plan and explicit validation gates.
- devops-tooling: Use it when the workflow also needs containers, automation, or artifact-publishing steps.
- cloud-design-patterns: Use it when the imported model must scale beyond a single demo pipeline.
Evaluation Report
Evaluation of the deepstream-import-vision-model skill before publication through NVSkills-Eval.
This benchmark summarizes 3-Tier Evaluation from NVSkills-Eval results for the skill. The goal is to document whether the skill is safe, discoverable, effective, and useful for agents before it is published for broader workflow use.
Evaluation Summary
- Skill:
deepstream-import-vision-model - Evaluation date: 2026-05-28
- NVSkills-Eval profile:
external - Environment:
local - Dataset: 5 evaluation tasks
- Attempts per task: 2
- Pass threshold: 50%
- Overall verdict: FAIL
Agents Used
claude-codecodex
Metrics Used
Reported benchmark dimensions:
- Security: checks whether skill-assisted execution avoids unsafe behavior such as secret leakage, destructive commands, or unauthorized access.
- Correctness: checks whether the agent follows the expected workflow and produces the correct final output.
- Discoverability: checks whether the agent loads the skill when relevant and avoids using it when irrelevant.
- Effectiveness: checks whether the agent performs measurably better with the skill than without it.
- Efficiency: checks whether the agent uses fewer tokens and avoids redundant work.
Underlying evaluation signals used in this run:
skill_execution(Skill Execution): verifies that the agent loaded the expected skill and workflow.skill_efficiency(Efficiency): checks routing quality, decoy avoidance, and redundant tool usage.accuracy(Accuracy): grades final-answer correctness against the reference answer.goal_accuracy(Goal Accuracy): checks whether the overall user task completed successfully.behavior_check(Behavior Check): verifies expected behavior steps, including safety expectations.token_efficiency(Token Efficiency): compares token usage with and without the skill.
Test Tasks
The benchmark dataset contained 5 evaluation tasks:
- Positive tasks: 3 tasks where the skill was expected to activate.
- Negative tasks: 2 tasks where no skill was expected.
- Unlabeled tasks: 0 tasks where positive/negative intent could not be inferred.
Task composition is derived from the evaluation dataset when possible. Entries with expected_skill set are treated as positive skill-activation cases, while entries with expected_skill: null are treated as negative activation cases.
Results
| Dimension | Num | claude-code | codex |
|---|---|---|---|
| Security | 8 | 68% (+13%) | 72% (+18%) |
| Correctness | 8 | 83% (-2%) | 89% (+13%) |
| Discoverability | 8 | 61% (+0%) | 80% (+1%) |
| Effectiveness | 8 | 80% (+2%) | 81% (+17%) |
| Efficiency | 8 | 52% (+2%) | 70% (+2%) |
Score values show skill-assisted performance. Values in parentheses show uplift versus the no-skill baseline when baseline data is available.
Tier 1: Static Validation Summary
Tier 1 validation passed with observations. NVSkills-Eval ran 9 checks and found 12 total findings.
Top findings:
- MEDIUM QUALITY/quality_correctness: SKILL_SPEC recommended field missing: 'metadata.tags' (
skills/deepstream-import-vision-model/SKILL.md) - MEDIUM SCHEMA/body_recommended_section: Missing recommended section: '## Instructions' (
skills/deepstream-import-vision-model/SKILL.md) - MEDIUM SCHEMA/body_recommended_section: Missing recommended section: '## Examples' (
skills/deepstream-import-vision-model/SKILL.md) - LOW QUALITY/quality_discoverability: Description very long (285 chars, recommend 50-150) (
skills/deepstream-import-vision-model/SKILL.md) - LOW QUALITY/quality_discoverability: No '## Purpose' section (
skills/deepstream-import-vision-model/SKILL.md)
Tier 2: Deduplication Summary
Tier 2 validation reported findings. NVSkills-Eval ran 2 checks and found 7 total findings.
Top findings:
- HIGH DUPLICATE/duplicate: Duplicate content found across scripts/deepstream/benchmark-ds.sh and scripts/deepstream/ds-kitti-dump.sh and scripts/deepstream/ds-perf-run.sh and scripts/deepstream/ds-single-stream.sh and scripts/deepstream/ds-sweep.sh and scripts/deepstream/extract-frame.sh and scripts/engine/benchmark-trtexec.sh and scripts/model/cleanup.sh and scripts/model/hf-download-config.sh and scripts/model/hf-list-files.sh and scripts/model/ngc-download.sh and scripts/model/ngc-list-files.sh and scripts/model/safetensors-to-onnx.sh and scripts/report/md-to-pdf.sh:
"(comment)" in scripts/deepstream/benchmark-ds.sh (lines 3-16) vs "(comment)" in scripts/deepstream/ds-kitti-dump.sh (lines 3-16) vs "(comment)" in scripts/deepstream/ds-perf-run.sh (lines 3-16) vs "(comment)" in scripts/deepstream/ds-single-stream.sh (lines 3-16) vs "(comment)" in scripts/deepstream/ds-sweep.sh (lines 3-16) vs "(comment)" in scripts/deepstream/extract-frame.sh (lines 3-16) vs "(comment)" in scripts/engine/benchmark-trtexec.sh (lines 3-16) vs "(comment)" in scripts/model/cleanup.sh (lines 3-16) vs "(comment)" in scripts/model/hf-download-config.sh (lines 3-16) vs "(comment)" in scripts/model/hf-list-files.sh (lines 3-16) vs "(comment)" in scripts/model/ngc-download.sh (lines 3-16) vs "(comment)" in scripts/model/ngc-list-files.sh (lines 3-16) vs "(comment)" in scripts/model/safetensors-to-onnx.sh (lines 3-16) vs "(comment)" in scripts/report/md-to-pdf.sh (lines 3-16) (scripts/deepstream/benchmark-ds.sh:3)
- HIGH DUPLICATE/duplicate: Duplicate content found within references/pipeline-run.md:
"# Hard constraint: num_streams <= engine max batch size — always" in references/pipeline-run.md (lines 437-442) vs "# Hard constraint: num_streams <= engine max batch size — always" in references/pipeline-run.md (lines 458-463) (references/pipeline-run.md:437)
- HIGH DUPLICATE/duplicate: Duplicate content found across references/report-generation.md and scripts/deepstream/ds-perf-run.sh:
"# Capture stream-0 instantaneous FPS (\K after **PERF:) — 1 value per line — so" in references/report-generation.md (lines 136-136) vs "(comment)" in scripts/deepstream/ds-perf-run.sh (lines 131-134) (references/report-generation.md:136)
- HIGH DUPLICATE/duplicate: Duplicate content found within references/pipeline-run.md:
"# 2=DeepStream NMS (dense heads: YOLO, SSD). Use 4 if engine has fused NMS output" in references/pipeline-run.md (lines 225-244) vs "# 2=DeepStream NMS (dense heads: YOLO, SSD). Use 4 if engine has fused NMS output" in references/pipeline-run.md (lines 401-414) (references/pipeline-run.md:225)
- HIGH DUPLICATE/duplicate: Duplicate content found within references/model-acquire.md:
"#### 2b-vi: onnxsim — Run After Export When Needed" in references/model-acquire.md (lines 273-282) vs "# Use the _sim.onnx for engine building if the original triggers ForeignNode errors" in references/model-acquire.md (lines 283-287) (references/model-acquire.md:273)
Publication Recommendation
The skill should be reviewed before NVSkills-Eval publication. Skill owners should address the findings above and rerun NVSkills-Eval to refresh this benchmark.
Changelog
All notable changes to the deepstream-import-vision-model skill will be documented in this file.
[2026-06-09] - Initial Import and Catalog Normalization
Added
- Imported
deepstream-import-vision-modelfromhttps://github.com/NVIDIA/skillsatskills/deepstream-import-vision-modelpinned to129a1087a1853f32a950e2f7bbc0fd7d57b9d422. - Added repo-standard
Anti-Patterns,Verification Protocol, portability, MCP fallback, and related-skills sections. - Preserved the upstream benchmark, signature, skill card, and bundled references or scripts for provenance and later refreshes.
Changed
- Normalized
SKILL.mdfrontmatter to the shared catalog schema withversion: "1.2"andlast_updated: 2026-06-09. - Moved upstream-only top-level metadata into the nested
metadatablock so validation, export, and downstream sync stay consistent.
Fixed
- Aligned the imported skill with this repository's maintained-skill requirements and downstream sync workflow.
[
{
"id": "deepstream-import-vision-model-001",
"question": "I want to import a HuggingFace object detection model into DeepStream. Describe the end-to-end workflow this skill should follow, including model acquisition, engine build, DeepStream validation, benchmarking, and report generation.",
"expected_skill": "deepstream-import-vision-model",
"expected_script": null,
"ground_truth": "The response should use the import-model workflow: inspect or download model assets, reject unsupported non-detection architectures, export or use ONNX, build TensorRT engines, create parser and nvinfer config, validate with a single-stream DeepStream run and KITTI output, run multi-stream benchmarks, and generate markdown, HTML, and PDF benchmark reports.",
"expected_behavior": [
"Read the relevant reference document before each phase rather than relying on memory.",
"Use the mandatory models/{model_name}/ directory structure.",
"Handle HuggingFace or NGC model acquisition and detect unsupported non-detection architectures early.",
"Build TensorRT engines with the prescribed naming pattern.",
"Run DeepStream validation before benchmarking.",
"Generate benchmark_report.md, benchmark_report.html, and benchmark_report_{model_name}.pdf."
]
},
{
"id": "deepstream-import-vision-model-002",
"question": "A YOLO object detection model exported from HuggingFace has dynamic ONNX dimensions. Explain how to build and configure it for DeepStream so the engine and nvinfer config are stable.",
"expected_skill": "deepstream-import-vision-model",
"expected_script": null,
"ground_truth": "The answer should inspect the ONNX model, create a static batch variant if needed, build TensorRT engines with batch-specific names, set infer-dims in the nvinfer config, use DeepStream NMS for pre-NMS YOLO outputs, and keep batch-size equal to the number of streams during DeepStream runs.",
"expected_behavior": [
"Inspect ONNX input and output shapes before engine build.",
"Create or use a static batch ONNX when dynamic dimensions would break TensorRT or DeepStream.",
"Name engines as {model}_dynamic_b{MAX_BS}.engine.",
"Set infer-dims to the explicit C;H;W input dimensions.",
"Use cluster-mode 2 for dense pre-NMS YOLO-style outputs.",
"Keep DeepStream batch-size equal to the number of input streams."
]
},
{
"id": "deepstream-import-vision-model-003",
"question": "During DeepStream validation for an imported detector, KITTI output has zero frames and NVENC is unavailable on the system. What should the skill do before producing a benchmark report?",
"expected_skill": "deepstream-import-vision-model",
"expected_script": null,
"ground_truth": "The skill should fail or stop before Step 7 when KITTI validation has zero frames or detection rate is below the threshold. For video output, it should use nvv4l2h264enc when available, fall back to theoraenc plus oggmux when NVENC is unavailable, or skip video creation if neither path is available, then report which mode was used.",
"expected_behavior": [
"Do not proceed to multi-stream benchmarking when KITTI frame count is zero.",
"Treat detection rate below 90 percent as a validation gate failure.",
"Do not use x264enc or openh264enc.",
"Use theoraenc plus oggmux as the fallback when NVENC is unavailable.",
"Skip video creation if neither NVENC nor theora fallback is available.",
"Report the selected video mode in the benchmark output."
]
},
{
"id": "deepstream-import-vision-model-004-negative",
"question": "Optimize SQL queries for a PostgreSQL reporting dashboard and add Redis caching. No model import or DeepStream runtime changes are needed.",
"expected_skill": null,
"expected_script": null,
"ground_truth": "The deepstream-import-vision-model skill should not be selected because the request is unrelated to model acquisition, TensorRT build, or DeepStream pipeline validation.",
"expected_behavior": [
"Do not activate deepstream-import-vision-model for this request.",
"Avoid model import, TensorRT, and DeepStream benchmarking instructions.",
"Respond with a generic fallback or suggest a relevant database-focused workflow."
]
},
{
"id": "deepstream-import-vision-model-005-negative",
"question": "How can I fine-tune a BERT model for sentiment analysis on my own dataset?",
"expected_skill": null,
"expected_script": null,
"ground_truth": "The deepstream-import-vision-model skill should not be selected because this request is unrelated to DeepStream object-detection model import or TensorRT/benchmark workflow.",
"expected_behavior": [
"Do not activate deepstream-import-vision-model for this request.",
"State that this is outside the DeepStream import-vision-model scope.",
"Suggest a relevant NLP model fine-tuning path instead."
]
}
]
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:
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; }
# Verify trtexec is available and check TRT version
TRTEXEC=$(which trtexec) || { echo "ERROR: trtexec not found in PATH — install TensorRT or check PATH"; exit 1; }
$TRTEXEC --help 2>&1 | head -3
dpkg -l | grep libnvinfer-bin
# Verify GPU is available
nvidia-smi --query-gpu=name,memory.total --format=csv,noheaderIf 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:
INSPECT_OUT=$(python3 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.
STEP4_START=$(date +%s.%N)
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# benchmarks/engines/ already exists from nv-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=$(echo "$STEP4_END - $STEP4_START" | bc)
echo "[Step 4] Engine build completed in ${STEP4_DURATION}s"Set the ENGINE variable — used by all subsequent trtexec and DeepStream runs:
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
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 nv-import-vision-model-report skill can find it with an exact path, not a wildcard.$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.logRun 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.$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}.logParse results and compute PEAK_GPU_STREAMS
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=$(echo "$STEP5_END - $STEP5_START" | bc)
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.
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 (MANDATORY before building engines)
Already done at the top of this skill via $TRTEXEC --help and dpkg -l | grep libnvinfer-bin. Do not repeat.
Docker vs Host Engine Builds
Docker-built engines may silently fail at runtime when loaded by host DeepStream (symptom: 0% GPU, pipeline stuck). Always build engines on the host using the same libnvinfer version as DeepStream (dpkg -l | grep libnvinfer-bin). Never mix TRT versions between engine builder and runtime.
Known Issues and Workarounds
--memPoolSize Flag Format — M vs MiB (CRITICAL silent failure)
- Correct:
--memPoolSize=workspace:32768M(suffixM= Mebibytes) - WRONG:
--memPoolSize=workspace:32768MiB— trtexec interpretsMiBas bytes, so32768MiBbecomes 32 KB. All tactics fail with "insufficient workspace". There is no parse warning; the only symptom isMemory Pools: workspace: 0.03125 MiBin 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)indynamic_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_levelto 2 via the Python TRT API (config.builder_optimization_level = 2). Prevents over-aggressive fusion; engines built this way are still compatible withtrtexec --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
TOTAL_DURATION=$(echo "$STEP4_DURATION + $STEP5_DURATION" | bc)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/NV Model Acquire — Steps 1-3
Acquire an ONNX model from Hugging Face, creating the mandatory model folder structure.
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/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:
[ -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"
fiMODEL_SOURCE(hforngc) drives category selection in Step 2MODEL_NAMEis 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:
# 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):
FILES="$(bash 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 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):
# HF: download from API via vetted helper. NGC: extracted from archive in Step 2d.
if [ "$MODEL_SOURCE" = "hf" ]; then
bash 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.jsonto 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
architecturesfield inconfig.jsonbefore continuing. If the architecture class ends in a non-detection suffix such asForImageClassification,ForSemanticSegmentation,ForInstanceSegmentation,ForPanopticSegmentation,ForDepthEstimation,ForMaskedLM,ForTokenClassification, orForCausalLM, 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 inForObjectDetection(or, for some DETR-family variants,ForConditionalDetection/ForZeroShotObjectDetection).
- Extract `labels.txt` from `config.json` — run this immediately after
config.jsonis in place (for HF models that is now; for NGC models this runs at the end of Step 2d):
python3 - <<EOF
import json, sys
with open("models/$MODEL_NAME/config/config.json") as f:
cfg = json.load(f)
# Primary: id2label (standard HF detection/classification format)
if "id2label" in cfg:
labels = [cfg["id2label"][str(i)] for i in range(len(cfg["id2label"]))]
# Fallback 1: label2id reversed
elif "label2id" in cfg:
labels = [k for k, v in sorted(cfg["label2id"].items(), key=lambda x: x[1])]
# Fallback 2: names dict/list (some YOLO HF repos)
elif "names" in cfg:
names = cfg["names"]
labels = [names[str(i)] for i in range(len(names))] if isinstance(names, dict) else list(names)
else:
print("ERROR: No label map found in config.json -- cannot create labels.txt", file=sys.stderr)
sys.exit(1)
with open("models/$MODEL_NAME/config/labels.txt", "w") as f:
f.write("\n".join(labels) + "\n")
print(f"labels.txt: {len(labels)} classes")
print(" " + ", ".join(labels[:5]) + (" ..." if len(labels) > 5 else ""))
EOFIf the script exits with error (no label map found), fail the pipeline with a clear error and exit — do not prompt the user, and never fall back to hardcoded COCO, ImageNet, or any other default list. This same script runs for HF and NGC — the only requirement is that config.json exists at models/$MODEL_NAME/config/config.json.
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:
# 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: Setup Isolated Virtual Environment
- ALWAYS use a dedicated venv for export tools. Never install optimum/transformers/torch system-wide.
- Use a single shared venv at
build/.venv_optimumacross all models —optimum,transformers,torch, andsafetensorsare heavy (~2-5 GB) and identical from one model to the next, so creating one per model wastes ~minutes of install time and GBs of disk every run. Theskills/deepstream-import-vision-model/scripts/model/safetensors-to-onnx.shhelper is built around this shared venv; align the skill-driven path with it.
mkdir -p build
VENV=build/.venv_optimum
if [ ! -x "$VENV/bin/optimum-cli" ]; then
python3 -m venv "$VENV"
source "$VENV/bin/activate"
pip install --upgrade pip
pip install optimum[exporters] torch transformers safetensors onnxruntime matplotlib numpy markdown
else
source "$VENV/bin/activate"
fi- For a new model that needs extra packages (e.g.
timmfor DETR-family backbones,onnxsim, or a differentoptimumpin),pip installthem into the existing shared venv rather than creating a new one:
source build/.venv_optimum/bin/activate
pip install timm # or: pip install 'optimum[exporters]<2.1'- The venv lives under
build/.venv_optimumat the repo root, keepingmodels/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_NAMEfrom older runs are still cleaned up byskills/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-Pto avoid changing the working directory:
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
.safetensorsfiles), also downloadmodel.safetensors.index.jsonand all shards
2b-iii: Try optimum-cli Export (Preferred) -- Max 3 Retries
optimum 2.1.0 removed the `onnx` subcommand. Ifoptimum-cli export onnxexits with "unknown command", pin an older version (pip install 'optimum[exporters]<2.1') or skip straight to Step 2b-iv (manualtorch.onnx.export). Theoptimum.exporters.onnxPython module is also gone in 2.1+.
- Attempt export using optimum-cli:
source build/.venv_optimum/bin/activate
optimum-cli export onnx \
--model models/$MODEL_NAME/hf_model \
--task object-detection \
--opset 17 \
models/$MODEL_NAME/onnx_export/- Common
--taskvalues for detection/vision models: object-detection-- DETR, YOLOS, Conditional DETRimage-classification-- ResNet, ViT, Swin, ConvNeXtimage-segmentation-- Mask2Former, SAMsemantic-segmentation-- SegFormer, UperNetzero-shot-object-detection-- OWL-ViT, Grounding DINO (if supported)- If export succeeds, copy the ONNX file to the
model/subdirectory:
cp models/$MODEL_NAME/onnx_export/model.onnx models/$MODEL_NAME/model/$MODEL_NAME.onnx- Retry policy: If the export fails, retry up to 3 times total with adjustments between attempts:
- Retry 1: Try a different
--taskvalue if the error suggests wrong task type - Retry 2: Try a different
--opsetversion (e.g., 14 or 16 instead of 17) - Retry 3: Try with
--no-post-processor other flags relevant to the error - After 3 failed attempts with optimum-cli, fall back to Step 2b-iv (manual torch.onnx.export)
2b-iv: Fallback -- Manual torch.onnx.export (If optimum fails) -- Max 3 Retries
- If optimum-cli fails after 3 retries (unsupported architecture), use manual export:
source build/.venv_optimum/bin/activate
python3 -c "
from transformers import AutoModelForObjectDetection, AutoConfig
import torch
model = AutoModelForObjectDetection.from_pretrained('models/$MODEL_NAME/hf_model')
model.eval()
# Create dummy input matching preprocessor_config.json dimensions
dummy = torch.randn(1, 3, 800, 800)
torch.onnx.export(model, dummy, 'models/$MODEL_NAME/model/$MODEL_NAME.onnx',
export_params=True, opset_version=17, do_constant_folding=True,
input_names=['pixel_values'],
output_names=['logits', 'pred_boxes'],
dynamic_axes={'pixel_values': {0: 'batch'},
'logits': {0: 'batch'},
'pred_boxes': {0: 'batch'}})
"- Adjust input/output names and shapes based on the model architecture
- Retry policy: If manual export fails, retry up to 3 times total with adjustments:
- Retry 1: Try a different
AutoModelclass (e.g.,AutoModel,AutoModelForImageClassification) - Retry 2: Try a different opset version or simplify dynamic_axes
- Retry 3: Try with
torch.onnx.export(..., operator_export_type=torch.onnx.OperatorExportTypes.ONNX_ATEN_FALLBACK) - After 3 failed attempts, stop and generate a failure report
Gotchas for recent PyTorch/transformers:
- PyTorch 2.11+ with onnxscript installed auto-upgrades opset to 18 even when opset_version=17 is requested. The resulting opset-18 ONNX is compatible with TRT 10.16 — accept it.- The dynamo backend (dynamo=True) may silently ignoredynamic_axesfor transformer models where attention reshape patterns bake the batch dimension into the graph. Verify exported input shapes withonnx.load(). For DETR-family models on TRT 10.16, prefer the dynamo path withtorch.export.Dim("batch", min=1, max=N)— it avoids the backbone-mask ForeignNode failure described innv-engine-build.
- The legacy TorchScript path (dynamo=False) crashes with transformers 5.5+ due tocreate_bidirectional_maskincompatibility.
- External data files:torch.onnx.exportmay producemodel.onnx.dataalongside the.onnx. Consolidate before TRT conversion:m = onnx.load(path, load_external_data=True); onnx.save(m, consolidated_path).
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:
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 errorsOnly 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:
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:
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.shintentionally does not remove it. cleanup.shremoves per-model artifacts (models/$MODEL_NAME/hf_model,models/$MODEL_NAME/onnx_export, and any legacybuild/.venv_$MODEL_NAMEleft over from older runs):
# Validated script; will refuse unsafe paths. Shared .venv_optimum is preserved.
bash skills/deepstream-import-vision-model/scripts/model/cleanup.sh "$MODEL_NAME"
# Preview without removing:
# bash 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:
# 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 skills/deepstream-import-vision-model/scripts/model/ngc-download.sh for details.
bash 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
.etltor.enginefile 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:
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.jsonfrom the archive and buildlabels.txt(same logic as HF path):
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"
# Now run the same labels.txt extraction as the HF path
python3 - <<EOF
import json, sys
with open("models/$MODEL_NAME/config/config.json") as f:
cfg = json.load(f)
if "id2label" in cfg:
labels = [cfg["id2label"][str(i)] for i in range(len(cfg["id2label"]))]
elif "label2id" in cfg:
labels = [k for k, v in sorted(cfg["label2id"].items(), key=lambda x: x[1])]
elif "names" in cfg:
names = cfg["names"]
labels = [names[str(i)] for i in range(len(names))] if isinstance(names, dict) else list(names)
else:
print("ERROR: No label map found in config.json -- cannot create labels.txt", file=sys.stderr)
sys.exit(1)
with open("models/$MODEL_NAME/config/labels.txt", "w") as f:
f.write("\n".join(labels) + "\n")
print(f"labels.txt: {len(labels)} classes")
print(" " + ", ".join(labels[:5]) + (" ..." if len(labels) > 5 else ""))
EOF
fiStep 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:
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_dataextension or similar) - Verify the download completed successfully and report file size
Timing
Record wall-clock time at the start and end of this skill:
STEP_START=$(date +%s.%N)
# ... all steps ...
STEP_END=$(date +%s.%N)
STEP_DURATION=$(echo "$STEP_END - $STEP_START" | bc)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.)
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
[ -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)
# Find TRT engine from nv-engine-build
ENGINE=$(ls models/$MODEL_NAME/benchmarks/engines/*_dynamic_b*.engine 2>/dev/null | head -1)
[ -z "$ENGINE" ] && { echo "ERROR: No engine found in models/$MODEL_NAME/benchmarks/engines/ — run Steps 4-5 first (references/engine-build.md)"; exit 1; }
MAX_BS=$(echo "$ENGINE" | grep -oP '_b\K[0-9]+(?=\.engine)')
# 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 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=12.8
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
MODEL_NAME_SAFE=$(echo "$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 (replace 9.0 with your installed minor version): apt-get install deepstream-9.0-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
STEP6_START=$(date +%s.%N)6a: Inspect Model Output Format
Verify output tensor shapes and value ranges before writing the parser:
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:
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.hand useNvDsInferObjectDetectionInfo(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.perClassPreclusterThresholdfor 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) * stridefor cx/cy andexp(raw) * stridefor 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]andpred_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, despiteconfig.jsonshowing"0": "N/A"). Skip the background class when iterating. DETR uses Hungarian matching — NMS is not needed; setcluster-mode=4(notnms-iou-threshold=0.0, which is a legacy key). - OWL-ViT / CLIP-based zero-shot detectors: outputs
logits [B, num_patches, num_classes]andpred_boxes [B, num_patches, 4]. Sigmoid activation (per-class independent scoring, not softmax). Boxes are(cx, cy, w, h)normalized[0,1]. Usecluster-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:
RTDetrImageProcessormay havedo_normalize=falseeven thoughimage_mean/image_stdfields exist. Whendo_normalize=false, the model expects[0,1]scaled input — setnet-scale-factor=1/255with 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— NOTlibnvds_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-pathat the system library andparse-bbox-func-nameat the TAO function. - KITTI dump from
deepstream-appmay 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-typeis a legacy/unknown key — nvinfer ignores it with a warning.network-type=0(Detector) is required to invokeparse-bbox-func-name.network-type=100(Other) does NOT invoke the custom bbox parser — it requiresoutput-tensor-meta=1for external post-processing.- Symptom of the wrong key: custom parse function is never called (zero detections, no parser debug output) — check that
network-type=0is 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):
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")
EOF6d: Build Parser Library
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 NvDsInferParseCustom6e: Create nvinfer Config File
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
EOFPath note: All paths are relative to the config/ directory where this file lives.net-scale-factordefaults to1/255— update to1.0if the model expects 0–255 input (verify via Step 6a).
Verify label count matches:
echo "labels.txt: $NUM_LABELS classes -> num-detected-classes=$NUM_LABELS"6f: Single-Stream Visual Validation
ENCODER RULE:
Primary encoder isnvv4l2h264enc(NVENC via V4L2) →.mp4.x264encandopenh264encare prohibited.
On systems where/dev/v4l2-nvencis unavailable, the approved fallback istheoraenc + oggmux
(LGPL; both ship in gst-plugins-base) →.ogv. Iftheoraenc/oggmuxare absent, video creation is skipped.
Use skills/deepstream-import-vision-model/scripts/deepstream/ds-single-stream.sh which handles this automaticallyand emits a DS_SINGLE_STREAM_MODE= marker the report parser reads.Primary (NVENC available):
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=0Fallback (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.
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=0Extract a frame to visually confirm bounding boxes — auto-detect which output file exists:
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
;;
esacIf 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-diris adeepstream-app[application]
property — it is not read by nvinfer directly. Appending it to thenvinfer config and running a gst-launch-1.0 ... nvinfer ... pipelinesilently produces zero KITTI files. Use the ds-kitti-dump.sh helper,which wrapsdeepstream-appwith the correct[application]section.
mkdir -p models/$MODEL_NAME/samples/kitti_output
bash 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 -10Validation gate: If KITTI_FILES == 0 or all files are empty, detections are broken. Do NOT proceed to Step 7.
# 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
fiSTEP6_END=$(date +%s.%N)
STEP6_DURATION=$(echo "$STEP6_END - $STEP6_START" | bc)
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:
mkdir -p models/$MODEL_NAME/benchmarks/ds
cat > models/$MODEL_NAME/benchmarks/ds/config_infer_ds_${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=../engines/${MODEL_FILENAME}_dynamic_b${MAX_BS}.engine
labelfile-path=../../config/labels.txt
batch-size=${MAX_BS}
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
EOFPath 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 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. Alwaysds_s${N}_run1.logandds_s${N}_run2.loginbenchmarks/ds/. The nv-import-vision-model-report skill reads these exact paths.
# 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 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=$(echo "$STEP7_RUN1_END - $STEP7_RUN1_START" | bc)
echo "[Step 7 Run 1] completed in ${STEP7_RUN1_DURATION}s"DS Run 2 — Validation at RT_STREAMS:
N=$RT_STREAMS
LOG_RUN2="models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run2.log"
STEP7_RUN2_START=$(date +%s.%N)
bash 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=$(echo "$STEP7_RUN2_END - $STEP7_RUN2_START" | bc)
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_STREAMSfrom trtexec is an overestimate — Run 1 at that count will show fps/stream well below 30. TheRT_STREAMS = floor(TOTAL_FPS_RUN1 / 30)formula above produces the correct NVDEC-limited ceiling. Do not pre-apply an efficiency factor toPEAK_GPU_STREAMSto compensate — the 2-run method measures overhead, it does not guess it.
If Run 2 is still not real-time (FPS/stream < 30): halve RT_STREAMS and retry once:
if [ "$RT_CONFIRMED" = "NO" ]; then
RT_STREAMS=$(python3 -c "import math; print(max(1, int(math.floor($RT_STREAMS / 2))))")
echo "Run 2 not real-time — retrying at $RT_STREAMS streams"
N=$RT_STREAMS
LOG_RUN2="models/$MODEL_NAME/benchmarks/ds/ds_s${N}_run2.log"
bash 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')")
echo "Retry: $N streams | FPS/stream=$FPS_RUN2 | Real-time: $RT_CONFIRMED"
fiCONSTRAINT: num_streams <= engine_max_bs always. Already enforced above via min(RT_STREAMS, MAX_BS).
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
TOTAL_67_DURATION=$(echo "$STEP6_DURATION + $STEP7_RUN1_DURATION + $STEP7_RUN2_DURATION" | bc)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/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 skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py \
models/$MODEL_NAME/reports/benchmark_report.md \
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- Callwkhtmltopdfdirectly — usemd-to-html-pdf.pywhich already calls it correctly
- Use md-to-pdf.sh — GFM+Mermaid design doc tool only, wrong CSS- Usepandoc,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.0with filesink + OSD) - DeepStream multi-stream benchmark (
deepstream-appwithenable-perf-measurement=1viads-perf-run.sh, PEAK_GPU_STREAMS and RT_STREAMS variants) - nvinfer config key fields (as an ini code block)
- Custom parser build command (
makewith 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.
STEP8_START=$(date +%s.%N)
MODEL_DIR="${ARGUMENTS%/}"
MODEL_NAME=$(basename "$MODEL_DIR")
# Locate engine — pick the LARGEST batch engine (sort -V ensures numeric sort, tail picks highest)
ENGINE=$(ls models/$MODEL_NAME/benchmarks/engines/*_dynamic_b*.engine 2>/dev/null | sort -V | tail -1)
[ -z "$ENGINE" ] && { echo "ERROR: No engine found in models/$MODEL_NAME/benchmarks/engines/ — run Steps 4-5 first (references/engine-build.md)"; exit 1; }
MAX_BS=$(echo "$ENGINE" | grep -oP '_b\K[0-9]+(?=\.engine)')
MODEL_FILENAME=$(basename "$ENGINE" | sed 's/_dynamic_b[0-9]*.engine//')
echo "Using engine: $ENGINE (MAX_BS=$MAX_BS)"
# 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 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 ds-run-pipeline
# 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)
DS_LOG_RUN2=$(ls 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):
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 viaos.environ.get(), appliesto_num()for safe numeric conversion (returnsNoneinstead of producing malformed JSON when a variable is unset), then usesjson.dumpto 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:
source build/.venv_optimum/bin/activateGenerate exactly 5 charts using matplotlib in models/{model_name}/reports/charts/. Use the script at 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 \ |
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:
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):
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=12
\`\`\`
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:
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:
python3 skills/deepstream-import-vision-model/scripts/report/md-to-html-pdf.py \
models/$MODEL_NAME/reports/benchmark_report.md \
skills/deepstream-import-vision-model/scripts/report/report-style.css \
models/$MODEL_NAME/reports/ \
$MODEL_NAMEThis 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:
STEP8_END=$(date +%s.%N)
STEP8_DURATION=$(echo "$STEP8_END - $STEP8_START" | bc)
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
#!/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))
ELAPSED_SEC=$(echo "scale=2; $ELAPSED_NS / $NS_PER_SEC" | bc)
FPS=$(echo "scale=1; $TOTAL_FRAMES / $ELAPSED_SEC" | bc)
REALTIME=$(echo "scale=2; $FPS / (${NUM_STREAMS} * ${VIDEO_FPS})" | bc)
echo ""
echo "=== Results ==="
echo "Wall time: ${ELAPSED_SEC}s"
echo "Total frames: ${TOTAL_FRAMES}"
echo "Throughput: ${FPS} img/s"
echo "Per-stream: $(echo "scale=1; $FPS / $NUM_STREAMS" | bc) fps"
echo "Real-time factor: ${REALTIME}x (${NUM_STREAMS} streams @ ${VIDEO_FPS}fps)"
echo "==============="
#!/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 ==="
#!/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
#!/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"
{
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
}
{
"args": ["--disable-dev-shm-usage"]
}
Related skills
FAQ
What does deepstream-import-vision-model do?
deepstream-import-vision-model is a Claude Code skill in the AI & Agent Building category.
When should I use deepstream-import-vision-model?
When you need to helps with ai & agent building tasks., or when deepstream-import-vision-model is a claude code skill in the ai & agent building category.
What are the main capabilities?
deepstream-import-vision-model; AI & Agent Building; AI-coding skill.