# 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"]