Spaces:
Running on RTX PRO 6000
Running on RTX PRO 6000
File size: 28,369 Bytes
8bc8124 6f8eac4 76adf06 6f8eac4 76adf06 6f8eac4 76adf06 6f8eac4 7215d2b 76adf06 6f8eac4 764fe7f 6f8eac4 7215d2b 6f8eac4 fda6bf9 6f8eac4 7215d2b 6f8eac4 7215d2b 6f8eac4 7215d2b 6f8eac4 7215d2b 6f8eac4 76adf06 6f8eac4 7215d2b 6f8eac4 7215d2b fda6bf9 6f8eac4 185aad5 6f8eac4 185aad5 6f8eac4 923558a 8bc8124 923558a 8bc8124 923558a 1079f5b cb00666 1079f5b cb00666 1079f5b 6f8eac4 76adf06 6f8eac4 7215d2b 6f8eac4 1603d8f 6f8eac4 7215d2b 6f8eac4 f427738 6f8eac4 db8c698 6f8eac4 0cd774a 6f8eac4 1079f5b 6f8eac4 a0d61cb 54673e6 6f8eac4 a0d61cb 1079f5b 16f8498 1079f5b cb00666 1079f5b cb00666 1079f5b 6f8eac4 bef3c2a 6f8eac4 f427738 76adf06 830ca4a 6f8eac4 76adf06 16f8498 cb00666 1079f5b cb00666 a0d61cb 1079f5b 6f8eac4 c5fce40 3a31d01 bef3c2a c5fce40 bef3c2a 6f8eac4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 | # deploy: 20261020-a3cb7f2-captions (simready-assets)
# GPU variant of the single-container MVP image for Hugging Face Spaces
# (Docker SDK): everything docker/Dockerfile.hf-space has (OpenSearch + Neo4j
# + Redis + deepsearch-api + asset-graph-service + nginx under supervisord),
# plus a *local* siglip2-triton process instead of routing SigLIP2 embeddings
# to NVCF β see docs/development.md#hf-space-design-notes
# for the decisions behind this file. Meant to be deployed to an HF Space running
# on a GPU hardware tier (T4 medium β see pipeline/hf_space.py's
# request_space_hardware call for the gpu variant); docker/Dockerfile.hf-space
# remains the free/cpu-basic, NVCF-backed variant β this is an additional
# deploy target, not a replacement (`SIGLIP2_BACKEND=nvcf` is unaffected).
#
# Builds the Triton half of this image the same way
# ../usdsearch/docker/Dockerfile.siglip2-triton does β FROM the bare, public
# nvcr.io/nvidia/tritonserver image, model_repo/ COPYed in from
# docker/hf-space/siglip2-model/ (vendored into *this* repo by
# scripts/extract_siglip2_model.sh, since HF's remote builder can reach
# neither ../usdsearch nor any private registry) β rather than FROM the
# prepackaged nvcr.io/nvidia/usdsearch/siglip2-triton image an earlier
# revision of this file used. Verified live why that prepackaged image was
# dropped: its published 1.4.0 tag bakes **FP32** weights (vision 4.4 GB +
# text 2.7 GB, inspected inside a pulled copy) β at this model's baked
# `instance_group: 2`, that's ~14 GB of resident GPU weight memory alone,
# and ~18 GB observed live in `nvidia-smi`, blowing past a T4's 16 GB VRAM.
# ../usdsearch's *current* services/siglip2-triton/model_repo checkout is
# already FP16-exported (vision 2.2 GB + text 1.4 GB β half the size, ~7 GB
# at instance_group: 2, comfortably under 16 GB) but was never republished
# under that NGC tag β vendoring this checkout's own model_repo directly
# sidesteps waiting on a new publish. See
# scripts/extract_siglip2_model.sh's own header for the full mechanism.
#
# nvcr.io/nvidia/tritonserver itself (the base, not the prepackaged image)
# is on the same *public* NGC catalog path already verified live this
# project's other public pulls (opensearchproject/opensearch, neo4j) use β
# no credentials needed, so Hugging Face's own remote builder can pull it.
#
# VERIFIED LIVE on a real GPU (RTX 4090, local β not a T4, but same
# execution path): full build, `docker run --gpus all`, all 6 Triton models
# READY, OpenSearch/Neo4j restore from S3, deepsearch-api + asset-graph-service
# healthy through nginx. GPU memory settled at ~10-11 GB (tritonserver
# ~10.3 GB + a smaller deepsearch-api CUDA-context allocation) β comfortably
# under a T4's 16 GB, confirming the FP32->FP16 switch actually fixed the
# budget problem, not just on paper. A real `/v3/deepsearch/search` call
# came back with genuine `search_type: text_to_vector` /
# `field: siglip2-embedding` hits (query "a red chair" correctly ranked
# office-chair assets top) β the local siglip2-triton round trip is real,
# not just "the process started". LLM query parsing
# (`/v3/deepsearch/llm_parse/query`) and VLM validation
# (`/v3/deepsearch/vlm_validate/search_result`) were also exercised live
# against the already-deployed NVCF Qwen endpoint
# (config/pipeline.env.srp-staging-controlled's LLM_ENDPOINT_BASE_URL) with
# $NVCF_GENAI_RUN_KEY as USDSEARCH_LLM_API_KEY β both returned real model
# output (a structured filter parse; a logprob-based
# `P(Yes)=... from first-token log-probs.` verdict), confirming this
# variant's NVCF-backed LLM/VLM path works the same as the NVCF/cpu-basic
# variant's.
# 3.9.0 rather than the pipeline's 3.8.0: 3.8.0's netty-handler 4.2.16
# (GHSA-c4c3-7fpv-j4q5, Critical) and httpcore5 5.4 jars were flagged by the
# nSpect container scan (2026-10-02). The Space bulk-loads the pipeline's
# NDJSON (pipeline/restore.py) rather than restoring a snapshot, so the
# versions don't need to match. Not named OPENSEARCH_VERSION: the jar patches
# below are tied to this exact version, and the pipeline's own
# OPENSEARCH_VERSION used to be passed through as a build-arg (and is still
# set as a Variable on the existing Spaces, which HF would pass along).
ARG HF_SPACE_OPENSEARCH_VERSION=3.9.0
# 5.26 (LTS) rather than the pipeline's 5.23: 5.23's bundled netty/jackson/
# jetty/shiro jars, APOC's json-smart and its JDK 17.0.12 were all flagged
# High by the nSpect container scan (2026-10-02). Same major version, so the
# pipeline's 5.23 dumps still load (verified locally).
ARG NEO4J_VERSION=5.26.31-community
ARG TRITONSERVER_BASE_IMAGE=nvcr.io/nvidia/tritonserver:26.05-py3
ARG PYTHON_IMAGE=python:3.12-bookworm
FROM opensearchproject/opensearch:${HF_SPACE_OPENSEARCH_VERSION} AS opensearch-src
FROM neo4j:${NEO4J_VERSION} AS neo4j-src
FROM ${TRITONSERVER_BASE_IMAGE}
WORKDIR /workspace
RUN mkdir -p /models
# Vendored model repo (see header comment) β ensembles, the Python-backend
# tokenizer/preprocessing models, and the two ONNX encoders' *config.pbtxt*
# only. The encoders' actual weight files (~3.6 GB combined β the ONNX
# externa-data blobs under each encoder's own 1/ subdirectory) are
# deliberately NOT baked here: a real `deploy-space-gpu` attempt 403'd with
# "Repository storage limit reached (Max: 1 GB)" β HF Space git repos have a
# default 1 GB storage quota, well under what baking these in needs. Instead
# they're restored from the artifact S3 bucket at container startup
# (pipeline/hf_space_seed.py --component=siglip2-model, run by
# supervisord-gpu.conf's [program:siglip2-triton] before tritonserver
# itself execs) β the same "runtime restore, not baked" pattern this image
# already uses for the OpenSearch index and Neo4j graph, just for a
# corpus-independent static artifact instead of per-run data. Publish
# weights to that S3 path with scripts/publish_siglip2_model_to_s3.sh
# whenever scripts/extract_siglip2_model.sh refreshes the vendored copy.
COPY docker/hf-space/siglip2-model/model_repo/ensemble_image_model /models/ensemble_image_model
COPY docker/hf-space/siglip2-model/model_repo/ensemble_text_model /models/ensemble_text_model
COPY docker/hf-space/siglip2-model/model_repo/image_preprocessing /models/image_preprocessing
COPY docker/hf-space/siglip2-model/model_repo/siglip2_vision_encoder_onnx/config.pbtxt /models/siglip2_vision_encoder_onnx/config.pbtxt
COPY docker/hf-space/siglip2-model/model_repo/siglip2_text_encoder_onnx/config.pbtxt /models/siglip2_text_encoder_onnx/config.pbtxt
RUN mkdir -p /models/siglip2_vision_encoder_onnx/1 /models/siglip2_text_encoder_onnx/1
COPY docker/hf-space/siglip2-model/model_repo/tokenizer /models/tokenizer
# Owned by uid 1000: [program:siglip2-triton] restores the encoder weights into /models as that user. Some Space
# hardware (rtx-pro-6000, 2026-10-21) runs the container as uid 1000 whatever USER says, so nothing may need root.
RUN chown -R 1000:1000 /models
# Python deps for the tokenizer + image_preprocessing Python-backend models β
# resolved from pyproject.toml + uv.lock (the source of truth) at build time,
# same as ../usdsearch/docker/Dockerfile.siglip2-triton itself does, so this
# doesn't need a separately-maintained requirements.txt that can drift.
# uv and virtualenv (below) are pinned so the third-party notice is
# reproducible: scripts/generate_third_party_notice.sh reads both pins from
# this file β re-run it after bumping either.
COPY docker/hf-space/siglip2-model/pyproject.toml docker/hf-space/siglip2-model/uv.lock /workspace/
RUN pip install --no-cache-dir uv==0.12.22 \
&& uv export --frozen --no-hashes --directory /workspace -o /workspace/requirements.txt \
&& pip install --no-cache-dir -r /workspace/requirements.txt \
&& rm /workspace/requirements.txt
COPY docker/hf-space/siglip2-model/PACKAGE-LICENSES /PACKAGE-LICENSES
# Attribution for anyone who pulls the image: this project's LICENSE plus the
# third-party notices (scripts/generate_third_party_notice.sh). Root-level in
# both build contexts (this repo for bake, the staged Space tree for HF);
# licenses/ stays next to the notice so its relative links resolve.
COPY LICENSE THIRD_PARTY_NOTICE.md /licenses/
COPY licenses /licenses/licenses
# Same two size/vulnerability trims ../usdsearch/docker/Dockerfile.siglip2-triton
# itself applies to this exact base image β carried over verbatim rather than
# dropped, since they're cheap and free up real space (~1 GB+ for Nsight
# Systems alone) on top of the FP16 savings above.
RUN rm -rf /usr/local/cuda-13.2/NsightSystems-cli-2026.2.1
USER root
# Non-root runtime uid/gid 1000, same convention docker/Dockerfile.hf-space
# uses for every data-writing process in that image (matches
# opensearchproject/opensearch's own "opensearch" user, 1000:1000). This
# base image is Ubuntu 24.04 ("noble") β not verified live whether uid 1000
# is already taken by a user the NGC tritonserver image itself creates, so
# this reuses it (rather than erroring) if it is: --chown=1000:1000 below
# only cares that the numeric id resolves consistently, not which username
# owns it.
RUN if id -u 1000 >/dev/null 2>&1; then \
echo "uid 1000 already exists as $(getent passwd 1000 | cut -d: -f1) β reusing it"; \
else \
groupadd -g 1000 nvs && useradd -u 1000 -g 1000 -m -d /home/nvs -s /usr/sbin/nologin nvs; \
fi
# python3.12-venv is requested
# explicitly (Ubuntu splits the venv module out of the base python3.12
# package on some images) since this base is noble rather than bookworm and
# hasn't been checked for what's preinstalled.
# libssl3t64 and the virtualenv upgrade mirror usdsearch 1.5.0-rc.7's
# docker/Dockerfile.siglip2-triton vulnerability fixes for this same base.
RUN apt-get update && apt-get install -y --no-install-recommends \
redis-server supervisor nginx curl python3.12-venv openssl libssl3t64 \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir --upgrade virtualenv==21.14.5 \
&& rm -f /etc/nginx/sites-enabled/default \
&& sed -i 's|^pid .*|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf \
&& chown -R 1000:1000 /etc/nginx/conf.d /var/lib/nginx /var/log/nginx
# nginx runs as uid 1000 (supervisord-gpu.conf): the pid file moves to /tmp, and the rendered config, temp and log
# dirs belong to that user. Some Space hardware (rtx-pro-6000, 2026-10-21) runs the container as uid 1000 whatever
# USER says, and nginx with user=root never started there (supervisord: "Can't drop privilege as nonroot user").
# The app venv: same /app/.venv path docker/Dockerfile.hf-space and
# docker/hf-space/supervisord*.conf both hardcode. Installed from the same
# repacked usdsearch wheels + constraints.txt as the NVCF variant β see that
# Dockerfile's header comment for how wheels/ is produced and refreshed.
# Upstream's own docker/Dockerfile.usdsearch compiles these wheels in a
# python:3.12-bookworm builder stage but runs them in a
# nvcr.io/nvidia/base/ubuntu:noble-20260217 *runtime* stage β the same OS
# family as this Triton image's own base β so installing them here follows
# upstream's own production direction (older-glibc build -> newer-glibc
# run), not a new cross-distro risk. Not yet verified live in this repo,
# though (see header comment).
COPY docker/hf-space/wheels/ /tmp/usdsearch-wheels/
# env -u PYTHONPATH: with the Triton base image's PYTHONPATH (DALI's wheel dir) visible, pip treated
# packages found there as installed and the venv came out without six (2026-09-29).
# The venv APIs start without PYTHONPATH (supervisord-gpu.conf), so the venv must be complete.
RUN python3.12 -m venv /app/.venv \
&& env -u PYTHONPATH /app/.venv/bin/pip install --no-cache-dir \
--find-links /tmp/usdsearch-wheels \
--constraint /tmp/usdsearch-wheels/constraints.txt \
/tmp/usdsearch-wheels/*.whl \
&& rm -rf /tmp/usdsearch-wheels \
&& chown -R 1000:1000 /app/.venv
# The vision checks' prepared asset views on disk (docker/hf-space/check_image_cache.py), on while the
# CHECK_IMAGE_CACHE_DIR Space Variable is set (pipeline/config.py). The .pth loads it in every venv python. The
# build fails unless it wraps the vendored llm_client and cached views come back byte-identical (--self-test), as PNG
# (the default) and as JPEG (USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg, the Space Variable prod runs with).
COPY docker/hf-space/check_image_cache.py docker/hf-space/check_image_cache.pth /tmp/check-image-hook/
RUN SP=$(env -u PYTHONPATH /app/.venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))') \
&& install -m 644 /tmp/check-image-hook/check_image_cache.py /tmp/check-image-hook/check_image_cache.pth "$SP/" \
&& rm -rf /tmp/check-image-hook \
&& env -u PYTHONPATH CHECK_IMAGE_CACHE_DIR=/tmp/check-image-selftest /app/.venv/bin/python -m check_image_cache --self-test \
&& env -u PYTHONPATH CHECK_IMAGE_CACHE_DIR=/tmp/check-image-selftest USDSEARCH_VISION_VALIDATION_IMAGE_FORMAT=jpeg \
/app/.venv/bin/python -m check_image_cache --self-test \
&& rm -rf /tmp/check-image-selftest
# hf-storage-api β see docker/Dockerfile.hf-space's own copy of this block
# for the full explanation (identical here; this variant serves the same
# corpus/storage backend, just with a different SigLIP2 backend).
COPY docker/hf-space/hf-storage-api/hf_storage_api /app/hf-storage-api/src/hf_storage_api
RUN GRPCIO_VERSION=$(env -u PYTHONPATH /app/.venv/bin/pip show grpcio | sed -n 's/^Version: //p') \
&& env -u PYTHONPATH /app/.venv/bin/pip install --no-cache-dir "grpcio-health-checking==${GRPCIO_VERSION}" \
&& env PYTHONPATH=/app/hf-storage-api/src /app/.venv/bin/python -c "import hf_storage_api.server" \
&& chown -R 1000:1000 /app/hf-storage-api
COPY --from=opensearch-src --chown=1000:1000 /usr/share/opensearch /usr/share/opensearch
# Keep only opensearch-knn: deepsearch-api's hybrid search fuses results
# client-side and the restored index uses only core analyzers + index.knn.
# The other bundled plugins carry netty/httpcore5 jars flagged High by the
# nSpect container scan (2026-10-02). Several passes because a plugin can't
# be removed while another still extends it. With opensearch-security gone,
# the entrypoint skips its DISABLE_SECURITY_PLUGIN handling.
RUN cd /usr/share/opensearch \
&& for pass in 1 2 3; do \
for p in $(ls plugins); do \
[ "$p" = opensearch-knn ] || bin/opensearch-plugin remove "$p" >/dev/null 2>&1 || true; \
done; \
done \
&& [ "$(ls plugins)" = opensearch-knn ] || { echo "plugins left: $(ls plugins)" >&2; exit 1; }
# Jackson jars no OpenSearch release fixes yet (nSpect, 2026-10-02):
# - lib/jackson-core 3.2.2 -> 3.2.3 (GHSA-7hhh-6rmp-j9qf), a patch-level
# drop-in from Maven Central, sha256-pinned. The old jar is removed by exact
# name, so a base bump that already changed it fails here β re-check then.
# - modules/ingest-geoip (jackson-databind/-core 2.22.2: GHSA-cxp5-3px4-pw24,
# GHSA-wv8q-qhhj-9h54, ...) removed: no ingest pipeline uses GeoIP.
RUN cd /usr/share/opensearch \
&& rm -r modules/ingest-geoip lib/jackson-core-3.2.2.jar \
&& curl -fsSL -o lib/jackson-core-3.2.3.jar \
https://repo1.maven.org/maven2/tools/jackson/core/jackson-core/3.2.3/jackson-core-3.2.3.jar \
&& echo "a0c17e7e665a5e95c82f5fb45c0fec2ae3dc556c80f0016452c9c84cd23f3817 lib/jackson-core-3.2.3.jar" | sha256sum -c - \
&& chown 1000:1000 lib/jackson-core-3.2.3.jar
# See docker/Dockerfile.hf-space's own (much longer) comment on this same
# block for the full k-NN/faiss library-path story β copied verbatim here
# since it's the identical opensearchproject/opensearch source image and the
# identical cross-image-copy cause.
ENV OPENSEARCH_HOME=/usr/share/opensearch \
OPENSEARCH_PATH_CONF=/usr/share/opensearch/config \
PATH="/usr/share/opensearch/bin:/app/.venv/bin:${PATH}" \
LD_LIBRARY_PATH=/usr/share/opensearch/plugins/opensearch-knn/lib \
discovery.type=single-node \
DISABLE_SECURITY_PLUGIN=true \
DISABLE_INSTALL_DEMO_CONFIG=true \
bootstrap.memory_lock=false \
OPENSEARCH_JAVA_OPTS="-Xms1g -Xmx1g -Djava.library.path=/usr/share/opensearch/plugins/opensearch-knn/lib"
# A Category pick sends one filter value per leaf, two Lucene clauses each, so
# the default cap (1,024) refused 513+ values with HTTP 400 (2026-10-19:
# Industrial + Residential + OEM = 519). 8192 takes ~4,000 values.
ENV indices.query.bool.max_clause_count=8192
COPY --from=neo4j-src /opt/java/openjdk /opt/java/openjdk
COPY --from=neo4j-src /var/lib/neo4j /var/lib/neo4j
# Jackson jars no Neo4j release fixes yet (nSpect, 2026-10-02 β 2026.09.0
# ships the same ones):
# - lib/jackson-core/-databind 2.22.2 -> 2.22.3 (GHSA-p6pp-m3f8-5c89,
# GHSA-7hhh-6rmp-j9qf, GHSA-cxp5-3px4-pw24, GHSA-wv8q-qhhj-9h54), patch-level
# drop-ins from Maven Central, sha256-pinned; old jars removed by exact name.
# - lib/parquet-jackson removed: it shades jackson 2.22.1 (same advisories)
# and has no fixed release. Only neo4j-admin's Parquet *import* uses it;
# this image only runs `neo4j-admin database load` and the server.
RUN cd /var/lib/neo4j/lib \
&& rm jackson-core-2.22.2.jar jackson-databind-2.22.2.jar parquet-jackson-1.18.0.jar \
&& curl -fsSL -o jackson-core-2.22.3.jar \
https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.22.3/jackson-core-2.22.3.jar \
&& curl -fsSL -o jackson-databind-2.22.3.jar \
https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.22.3/jackson-databind-2.22.3.jar \
&& printf '%s %s\n' \
8a501126a385b25841915d839508f8a66e2a0dbc8a6709d055ef3b3e852b094c jackson-core-2.22.3.jar \
556db5439e206114346043f68d200497dc96a0bca62a360a81784092ebd0e0a9 jackson-databind-2.22.3.jar \
| sha256sum -c -
RUN mkdir -p /data /logs && chmod 777 /data /logs
RUN cp /var/lib/neo4j/labs/apoc-*-core.jar /var/lib/neo4j/plugins/
RUN { \
echo "dbms.security.auth_enabled=false"; \
echo "server.memory.heap.initial_size=1G"; \
echo "server.memory.heap.max_size=1G"; \
echo "server.memory.pagecache.size=512M"; \
} >> /var/lib/neo4j/conf/neo4j.conf
ENV NEO4J_HOME=/var/lib/neo4j \
PATH="/var/lib/neo4j/bin:/opt/java/openjdk/bin:${PATH}"
COPY docker/hf-space/logging.yml /logging.yml
COPY docker/hf-space/search_fields.yaml /app/search_fields.yaml
# The query parser's system prompt β same as docker/Dockerfile.hf-space, see its comment.
COPY docker/hf-space/llm_parse_prompt.txt /app/llm_parse_prompt.txt
# The GPU-variant supervisord config β same six programs as
# docker/hf-space/supervisord.conf plus [program:siglip2-triton], and
# deepsearch-api's own command additionally waits on Triton's readiness
# before execing uvicorn. See that file's header for the port-collision
# reasoning (Triton's default HTTP port 8000 clashes with deepsearch-api's
# own uvicorn; remapped to 8003 here).
COPY docker/hf-space/supervisord-gpu.conf /etc/supervisor/conf.d/hf-space.conf
# nginx's optional readiness gate (LIBRARY_MEDIA_GATE; see the script's header).
COPY docker/hf-space/media-gate.sh /usr/local/bin/media-gate.sh
COPY docker/hf-space/nginx.conf.template /etc/nginx/hf-space.conf.template
COPY services/explorer/dist /usr/share/nginx/explorer
COPY --chown=1000:1000 pipeline /app/hf-space-pipeline/pipeline
# package-download (supervisord's program of that name) runs from this copy on the
# venv deepsearch-api already brought (fastapi, uvicorn, grpcio, the storage_api
# stubs, search_utils): fail the build, not the Space, if that ever stops importing.
RUN cd /app/hf-space-pipeline && env -u PYTHONPATH /app/.venv/bin/python -c "import pipeline.package_download.app, pipeline.package_download.source, pipeline.package_download.sizes, search_utils.url_mapping"
ARG ARTIFACT_S3_URI
ARG ARTIFACT_S3_REGION=us-east-2
ARG CORPUS_NAME
ENV ARTIFACT_S3_URI=${ARTIFACT_S3_URI} \
ARTIFACT_S3_REGION=${ARTIFACT_S3_REGION} \
CORPUS_NAME=${CORPUS_NAME}
# βββ deepsearch-api config baked for this corpus/run ββββββββββββββββββββββββ
# Same as docker/Dockerfile.hf-space's equivalent block, minus every
# NVCF-specific ARG/env (SIGLIP2_NVCF_ENDPOINT/FUNCTION_ID and their
# downstream SIGLIP2_TRITON_SERVER_* values) β this variant never talks to
# NVCF at all. SIGLIP2_TRITON_SERVER_URL instead points at the local
# siglip2-triton process's gRPC port (8001, loopback-only β nginx remains
# the sole externally-proxied port, same as every other backend process in
# this image), with no SSL and no auth headers, since nothing external ever
# reaches it.
ARG ES_NAME
ARG S3_STORAGE_BUCKET_NAME
ARG S3_STORAGE_REGION_NAME=us-west-2
ARG STORAGE_BACKEND_TYPE=s3
# DOWNLOAD_ENABLED / HF_STORAGE_API_TOKEN / SUBMIT_CONTENT_URL / STORAGE_API_* / DOWNLOAD_* /
# PACKAGE_DOWNLOAD_* β see
# docker/Dockerfile.hf-space's own copies of these comments for the full
# explanation.
# USDSEARCH_URL_MAPPING_MODE/_RULES β hf_space_build_args() derives them from
# the repos (and pinned revisions) this run indexed; see url_mapping_rules().
ARG USDSEARCH_URL_MAPPING_MODE=off
ARG USDSEARCH_URL_MAPPING_RULES=[]
ARG DOWNLOAD_ENABLED=false
ENV HF_STORAGE_API_TOKEN= \
DOWNLOAD_ENABLED=${DOWNLOAD_ENABLED} \
SUBMIT_CONTENT_URL=https://huggingface.co/datasets/nvidia/simready-central/discussions \
STORAGE_BACKEND_TYPE=${STORAGE_BACKEND_TYPE} \
S3_STORAGE_BUCKET_NAME=${S3_STORAGE_BUCKET_NAME} \
S3_STORAGE_REGION_NAME=${S3_STORAGE_REGION_NAME} \
opensearch_host=http://localhost:9200 \
opensearch_index_name=${ES_NAME}-ver5.0 \
opensearch_image_cache_index_name=${ES_NAME}-ver4.0-image-cache \
opensearch_use_ssl=false \
REDIS_URL=redis://localhost:6379 \
DEEPSEARCH_BACKEND_ASSET_GRAPH_SERVICE_URL=http://127.0.0.1:8010 \
ASSET_GRAPH_SERVICE_ENDPOINT=http://127.0.0.1:8010 \
ENABLE_ACCESS_VERIFICATION=False \
STORAGE_REQUIRE_AUTH=False \
VERIFY_ASSET_EXISTENCE=False \
verify_access=false \
ASSERT_ADMIN_USER=false \
USDSEARCH_VISION_VALIDATION_ENABLED=false \
USDSEARCH_LLM_PARSING_ENABLED=false \
# Validation limits are per deepsearch-api process: 32 checks in flight and a 31k-verdict cache in
# each of the 16 processes (DEEPSEARCH_API_PROCESSES below), 512 and ~500k in all (Redis keeps every verdict
# anyway). One search's page runs at most 16 at once (usdsearch's BATCH_CONCURRENCY). 2026-10-20 lab, l4x4
# shape with the check-image cache: 16 per process capped the engine side at ~155 checks/s (p95 of all badges
# 14 s at 80 users); 32 held 3.2 s up to 240 users.
USDSEARCH_VISION_VALIDATION_MAX_CONCURRENT=32 \
USDSEARCH_VISION_VALIDATION_CACHE_SIZE=31250 \
USDSEARCH_LLM_PARSING_FIELDS_FILEPATH=/app/search_fields.yaml \
USDSEARCH_LLM_PARSING_PROMPT_FILEPATH=/app/llm_parse_prompt.txt \
# Business telemetry β same as docker/Dockerfile.hf-space, see its comment.
OTEL_SDK_DISABLED=false \
USE_SEARCH_TELEMETRY=true \
OTEL_TRACES_EXPORTER=false \
BACKEND_CONFIG_STR="backend_name: es-sample" \
DEFAULT_CUTOFF_THRESHOLD=0 \
HYBRID_SEARCH_SCORING_CONFIG_VECTOR_FIELDS="{\"siglip2-embedding.embedding\":{\"enabled\":true,\"weight\":1,\"field_name\":\"siglip2-embedding.embedding\",\"dimension\":1536}}" \
# Text-leg default β same as docker/Dockerfile.hf-space, see its comment.
HYBRID_SEARCH_HYBRID_TEXT_CONFIG_WEIGHT=0.9 \
HYBRID_SEARCH_HYBRID_TEXT_CONFIG_CROSS_FIELD_OPERATOR=or \
HYBRID_SEARCH_HYBRID_TEXT_CONFIG_FIELDS="[{\"field\":\"name\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name.simple\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name.standard\",\"nested\":false,\"enabled\":true,\"weight\":2,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"usd_properties.value\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"usd_properties.key\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"tags.tag\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"tags.value\",\"nested\":true,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path.tree\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"path.tree_reversed\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"__VISION_METADATA_FIELDS__\",\"nested\":false,\"enabled\":true,\"weight\":1,\"match_type\":\"fuzzy\",\"fuzzy_max_expansions\":1,\"wildcard\":true},{\"field\":\"name\",\"nested\":false,\"enabled\":true,\"weight\":5,\"match_type\":\"exact\",\"analyzer\":\"keyword\",\"wildcard\":false},{\"field\":\"name.standard\",\"nested\":false,\"enabled\":true,\"weight\":5,\"match_type\":\"exact\",\"analyzer\":\"keyword\",\"wildcard\":false}]" \
USDSEARCH_URL_MAPPING_MODE=${USDSEARCH_URL_MAPPING_MODE} \
USDSEARCH_URL_MAPPING_RULES=${USDSEARCH_URL_MAPPING_RULES} \
STORAGE_API_GRPC_ENDPOINT=127.0.0.1:50051 \
STORAGE_API_SSL=false \
STORAGE_API_IGNORE_FILEFOLDER_API=true \
DOWNLOAD_MAX_CONCURRENT=2 \
DOWNLOAD_QUEUE_TIMEOUT=120 \
DOWNLOAD_MAX_BUNDLE_BYTES=2000000000 \
PACKAGE_DOWNLOAD_PORT=8020 \
PACKAGE_DOWNLOAD_MAX_CONCURRENT=4 \
PACKAGE_DOWNLOAD_QUEUE_TIMEOUT=120 \
PACKAGE_DOWNLOAD_MAX_BUNDLE_BYTES=2000000000 \
STORAGE_API_THUMBNAIL_METADATA_FIELDS="[\"hf:thumbnail_url\"]" \
SIGLIP2_TRITON_SERVER_URL=localhost:8001 \
TRITON_SERVER_URL=localhost:8001 \
SIGLIP2_TRITON_SERVER_SSL=false \
# deepsearch-api processes, ONE uvicorn worker each, behind nginx's least_conn
# upstream (supervisord-gpu.conf, nginx.conf.template). 16 for l4x4 (48 vCPU):
# 2026-10-20 lab, l4x4 shape, 32 checks each: 16 processes held p95 of all badges at
# 3.2 s up to 240 users (6.3 s at 300, CPU-bound); 6 processes reached ~100 users
# with the CPU under 30 %. On a10g-large (12 vCPU) use 6: 8 cost ~20 % more CPU there. Must equal supervisord-gpu.conf's
# numprocs: nginx renders its upstream from this, so never set it as a Space
# Variable. DEEPSEARCH_API_WORKERS is no longer read (a leftover Space Variable
# of that name does nothing).
DEEPSEARCH_API_PROCESSES=16 \
PORT=7860
ARG RUN_ID
ARG CORPUS_SHA256
ARG USDSEARCH_GIT_SHA
LABEL org.opencontainers.image.title="usdsearch-hf-space-simready-central-gpu" \
org.opencontainers.image.description="Single-container MVP (OpenSearch+Neo4j+Redis+deepsearch-api, SigLIP2 via local Triton on GPU) for Hugging Face Spaces" \
com.nvidia.usdsearch.run_id="${RUN_ID}" \
com.nvidia.usdsearch.corpus_sha256="${CORPUS_SHA256}" \
com.nvidia.usdsearch.usdsearch_git_sha="${USDSEARCH_GIT_SHA}"
# Longer start-period than docker/Dockerfile.hf-space's 10m: this variant
# additionally waits on Triton loading ~3.6 GB of ONNX weights onto the GPU
# before deepsearch-api's own uvicorn execs (see supervisord-gpu.conf).
# Verified live against a small (1488-asset) dev corpus: Triton model load
# took ~10s and the whole stack was healthy within ~20s total β 15m is
# deliberately still a generous upper bound, not a tight one, since a larger
# production corpus's OpenSearch bulk load / Neo4j offline load (the same
# restore path docker/Dockerfile.hf-space already budgets 10m for) can take
# much longer than this dev corpus did.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15m --retries=3 \
CMD curl -sf "http://localhost:${PORT}/health" || exit 1
EXPOSE 7860
ENTRYPOINT ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/hf-space.conf"]
|