Skip to content

feat(tensorflow): add TF Serving 2.20 inference DLC on AL2023 - #6243

Open
bhanutejagk wants to merge 71 commits into
mainfrom
tensorflow-2.20-inference
Open

feat(tensorflow): add TF Serving 2.20 inference DLC on AL2023#6243
bhanutejagk wants to merge 71 commits into
mainfrom
tensorflow-2.20-inference

Conversation

@bhanutejagk

@bhanutejagk bhanutejagk commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

The first TensorFlow inference DLC on the main (v2) branch. TensorFlow Serving 2.20.0 serving TF 2.21-trained SavedModels on AL2023 + CUDA 12.9.1 + Python 3.12, SageMaker-only, x86-only, with single-model and Multi-Model Endpoint (MME) support.

  • New docker/tensorflow/inference/2.20/ tree — Dockerfile.cuda and Dockerfile.cpu on AL2023, uv-driven dependency management, cuDNN pinned via nvidia-cudnn-cu12==9.24.0.43.
  • nginx-mod-njs installed from AL2023 core (--releasever latest) — no source-build stage needed.
  • SageMaker handler scripts ported from master TF 2.19's build_artifacts/sagemaker/ with 3 inherited-master bug fixes surfaced by review: GET /models/{name} returning 500, missing traversal guard, ProcessLookupError leak on DELETE.
  • New GitHub Actions workflows (CPU + CUDA PR triggers, shared pipeline, SageMaker integration tests, workflow-dispatch-only autorelease with security-test gating).
  • 12 SageMaker integration tests + 1 sanity test file (30 test functions) covering single-model, MME dynamic load, MME traversal, MME concurrency, batch transform, content-types (JSON + CSV), custom inference.py, error paths, model versioning, nginx/gunicorn env tuning, TFS batching, and GPU-only Conv2D exercising cuDNN kernel dispatch.

Why this PR

This is the first TensorFlow inference image on main (v2 architecture). The master branch's TF 2.19 inference image is on Ubuntu 22.04 + Python 3.10 + CUDA 12.2. Porting to main's AL2023 + Python 3.12 + CUDA 12.9.1 substrate is the migration this PR delivers, aligned with the TF 2.21 training PR (#6107) already merged on main.

Test plan

  • CI builds image successfully (both CPU and CUDA variants)
  • Sanity tests pass (imports, tensorflow_model_server --version, cuDNN sub-library presence via ldconfig -p, handler artifacts, entrypoint executable, exec-hands-off-PID1 assertion for SIGTERM-critical scripts)
  • Security test / ECR vulnerability scan passes
  • Telemetry test passes (environment + instance)
  • SageMaker integration tests pass on both CPU and CUDA (~41m CPU / ~53m CUDA)
  • Pre-commit passes (ruff, dockerfmt, mdformat, gitleaks)
  • OSS compliance check passes

After merge

Follow-up PR + tasks required before customer-facing release:

  1. Flip to production — set environment : production in .github/config/image/tensorflow/2.20-inference-sagemaker-{cpu,cuda}.yml
  2. Add cron schedule to autorelease — add schedule: trigger (Tue/Thu 17:30 UTC, matching training) to autorelease-tensorflow-inference-sagemaker-{cpu,cuda}.yml
  3. Publish release logic CRCR-293646336 (tag template + inference parity tests in DLContainersReleaseLogicPython)
  4. Post-release tasks:
    • Update available images doc (docs/src/data/tensorflow-inference/*.yml)
    • Add images to Security Scanning Dashboard
    • Update DLC Support Policy (ga date, eop = ga + 12 months)
    • Create release notes (docs/tensorflow/ tree)
    • Update new image to ADC regions (ConsumableImages CR + ADC CDK + Special Partitions CDK)
    • Public ECR gallery content update

Pre-review notes

  • Known limitation — LSTM/GRU inference not supported on TFS 2.20: TFS 2.20 cannot compile TF 2.21 Keras 3 LSTM while_loop graphs under its XLA/JIT autotuner. Same forward-compat gap class as DecodeJxl. Not a deferral — LSTM/GRU inference is simply out-of-scope for this image; customers should use TFS 2.21+ or CPU inference for those workloads.

Related

First TF inference DLC on the v2 (main) branch. Mirrors master's TF 2.19
inference image but ports to AL2023 + CUDA 12.9.1 + Python 3.12, switches
to uv-driven dependency management, and rebuilds nginx-mod-njs from source
(no AL2023 RPM exists). SageMaker only, x86 only, single-model + MME
support preserved.

- Dockerfile.cuda + Dockerfile.cpu with builder-njs source-build stage
- Ports SageMaker handler scripts (Falcon + nginx + njs + multi_model_utils)
  from master TF 2.19 build_artifacts/sagemaker/ byte-for-byte
- GitHub Actions workflows mirror PR #6107 (TF 2.21 training) shape
- SageMaker integration tests for single-model and MME endpoints
- ECR scan allowlist overlay at tensorflow/tensorflow-2.20.json

TFS 2.20.0 binary copied from tensorflow/serving:2.20.0-devel-gpu image.
tensorflow-serving-api installed with --no-deps to avoid pulling 600 MB
of TF framework. framework: "tensorflow" + job_type: "inference"
matches the cross-team release-logic convention (training and inference
share the framework slug, distinguished by job_type).

Known follow-ups (not blocking this PR):
- PR #6107 (TF 2.21 training) will add tensorflow/framework_allowlist.json
  (shared base) once it merges. CI security-test may fail until then.
- Separate change against the release-logic config needed to add tensorflow
  entry to frameworks.yml before image can release to prod.
Bhanu Teja Goshikonda added 10 commits June 15, 2026 11:21
Run pre-commit hooks (ruff-format, ruff-check, requirements-txt-fixer,
trailing-whitespace) on Phase 1-6 files. Mechanical reformatting plus
noqa annotations for intentional E402 imports after gevent monkey-patch
in python_service.py and one F841 unused variable in serve.py
(preserved as-is from master TF 2.19 vendored handler).
Re-indent RUN/ENV continuation lines from 4-space to 2-space per
dockerfmt's normalization. Mechanical reformatting only — no logic
changes. Closes the dockerfmt CI failure on PR #6243.
The builder-njs stage's `tar xzf` invocation needs gzip, which AL2023's
base image does not include by default. CI build failed at the nginx
source extraction step on PR #6243. Add `gzip xz` to the existing dnf
install line in both Dockerfile.cuda and Dockerfile.cpu builder-njs
stages.
nginx ./configure --with-compat with the njs dynamic module auto-enables
the HTTP XSLT module, which requires libxml2 and libxslt development
headers. AL2023 base lacks these. Adding to the existing dnf install
line in both Dockerfile.cpu and Dockerfile.cuda builder-njs stages.
nginx + njs `make modules` with the http-only --add-dynamic-module
produces ngx_http_js_module.so but not the stream variant.
SageMaker HTTP-based model serving uses only the http njs module
(tensorflowServing.js routes /invocations and /ping). Removing
the unused cp in builder-njs, the orphan COPY in runtime-base,
and the comment reference in both Dockerfile.cuda and Dockerfile.cpu.
Training PR's sagemaker-test job avoids the test/conftest.py fabric
import not by installing fabric, but by having test/tensorflow/pytest.ini
cap pytest rootdir at test/tensorflow/. Mirror that mechanism here.

- Add test/tensorflow/pytest.ini ([pytest] stanza)
- Match training's requirements.txt content exactly (boto3, pytest,
  sagemaker>=3.0.0) at test/tensorflow/integration/inference/
- Both inference workflows now install via -r requirements.txt
CVE-2025-23339 and CVE-2025-23308 in cuda-toolkit-config-common
12.9.79. Fix requires CUDA 13 major version bump which is tracked
as a separate currency update; CUDA 12.9 is pinned to match the
AL2023 base image and parallel TF 2.21 training PR.
ecr_scan.py constructs the overlay path as
<framework>/<framework>-<framework_version>.json. With
framework_version=2.20.0 (full semver) the lookup is
tensorflow-2.20.0.json, not the short tensorflow-2.20.json
we previously committed. Rename to match so the two CUDA
toolkit CVEs are actually picked up.
The PyPI sagemaker package v3.x removed the top-level sagemaker.Session
class along with sagemaker.tensorflow.serving.TensorFlowModel and
sagemaker.multidatamodel.MultiDataModel. Our conftest used the legacy v2
API. Rewrite to use boto3 directly (sagemaker, sagemaker-runtime, s3
clients) so tests work with the >=3.0.0 pin and are SDK-version
independent. boto3 maps 1:1 to the create_model / create_endpoint_config
/ create_endpoint / invoke_endpoint flow these tests already needed,
which is simpler than adopting v3's heavier ModelBuilder abstraction
for integration tests. The sagemaker dep is dropped from
requirements.txt since it is no longer imported.
Replaces commit 4b99d0f which used boto3 directly. The SageMaker
Python SDK v3 is the supported entrypoint for these tests; v3 removed
the v2 sagemaker.Session, sagemaker.tensorflow.serving.TensorFlowModel,
and sagemaker.multidatamodel.MultiDataModel classes the old fixtures
relied on.

For DLC integration tests we already supply image_uri and a pre-built
model.tar.gz, so ModelBuilder's auto-detection adds no value. We use
the v3 sagemaker-core resource layer directly (the same surface
ModelBuilder calls underneath):

- conftest sagemaker_session fixture uses sagemaker.core.helper.session_helper.Session
  (default_bucket / upload_data); cleanup_endpoint uses Endpoint.get(...).delete(),
  EndpointConfig.get(...).delete(), Model.get(...).delete()
- single-model test uses Model.create(primary_container=ContainerDefinition(...))
  + EndpointConfig.create([ProductionVariant(...)]) + Endpoint.create() with
  endpoint.wait_for_status("InService") and endpoint.invoke(...)
- MME test expresses the multi-model contract directly:
  ContainerDefinition(mode="MultiModel", model_data_url=<S3 prefix>) and
  endpoint.invoke(target_model="modelN.tar.gz")
- sagemaker>=3.0.0 retained in requirements.txt
Bhanu Teja Goshikonda added 2 commits July 24, 2026 12:40
Main refactored _reusable.{sanity,security,telemetry}-tests.yml to
accept only config-file and image-uri, deriving all other metadata
from the config-file's contents. Rewrite our 6 caller sites to pass
the new contract, mirroring main's own TF PR workflows.
@bhanutejagk
bhanutejagk marked this pull request as ready for review July 24, 2026 23:33
.github/scripts/buildkitd.sh no longer exists on main after the
CI refactor. Mirror .github/actions/build-image/action.yml's
buildkitd setup step in both TF 2.20 inference PR workflows.
@aws aws deleted a comment from github-actions Bot Jul 25, 2026
Bhanu Teja Goshikonda added 4 commits July 24, 2026 17:09
Three coordinated fixes to the sagemaker-test job:

1. Delete test/tensorflow/pytest.ini. Its premise ("mirror training's
   pytest rootdir cap") was wrong — training never had this file.
   The file blocks test/conftest.py from loading, hiding the
   --image-uri pytest addoption and breaking training's tests when
   they piggyback on our checkout.

2. Install test/requirements.txt (which pulls in fabric) alongside
   the inference-specific requirements. test/conftest.py transitively
   imports fabric via test_utils.aws; without it pytest ImportErrors.
   Mirrors training's tensorflow.tests-sagemaker.yml install pattern.

3. Standardize on TEST_IMAGE_URI env var (matches what the workflow
   already sets, and matches PyTorch/openfold3/EFA SM conventions).
   Rename INFERENCE_IMAGE_URI reads in the conftest. The bespoke
   INFERENCE_IMAGE_URI name had no other users.
…ocation

Main's PR #6250 refactor moved:
- scripts/common/ -> scripts/docker/common/
- scripts/telemetry/ -> scripts/docker/telemetry/

Our Dockerfiles referenced the old paths — same class of merge
fallout as the earlier .github/scripts/buildkitd.sh ->
.github/actions/build-image/buildkitd.sh relocation. Update all 8
affected COPY lines (4 per Dockerfile).
…factor main

Three coordinated fixes to unblock sagemaker-test from silent-skip
behavior after the merge from main:

1. conftest reads SM_ROLE_ARN (matches training's tensorflow.tests-
   sagemaker.yml and what our workflow already exports).
2. Workflow path triggers and paths-filter reference scripts/docker/
   {common,telemetry}/** (post-PR-6250 relocation).
3. Image configs use post-refactor schema (image/metadata/build/
   release blocks). Workflow yq lookups updated to .metadata.* and
   .build.* accordingly. Old `common:` schema caused resolve-image-
   uri to derive URI ending in /null and sanity/security/telemetry
   to skip silently.
Migrate PR #6243 to match origin/main's post-refactor conventions:

- Move image configs into .github/config/image/tensorflow/ subfolder
- Move handler scripts to scripts/docker/tensorflow/inference/
- Adopt .github/actions/build-image composite (drops inlined buildx,
  labels, build-args, and downstream URI reconstruction)
- Delete versions-*.env (pins now live in image config build: block)
- Rename workflows to tensorflow.pr-2.20-inference-{cpu,cuda}.yml
- Add tensorflow.pipeline-inference.yml as parallel reusable pipeline
  (inference-only sagemaker-test; no exit-5 tolerance)

Aligns file layout with all other frameworks (pytorch, ray, sglang,
etc.) and unlocks the shared build-arg / pre-build-hook mechanism.
Bhanu Teja Goshikonda added 7 commits July 27, 2026 12:41
Our conftest fixture reads env var TEST_IMAGE_URI, but the pipeline
was passing --image-uri as a CLI flag. Neither was set → fixture
would pytest.skip("TEST_IMAGE_URI not set") → both tests silently
skipped and sagemaker-test reported green without asserting
anything.

Align workflow with conftest: set TEST_IMAGE_URI in step env,
drop --image-uri CLI flag. Matches pytorch.tests-sagemaker.yml
and openfold3.tests-sagemaker.yml peer convention.
Move the sagemaker-test job body from tensorflow.pipeline-inference.yml
into a new tensorflow.tests-sagemaker-inference.yml reusable, matching
the pattern used by every other framework (pytorch.tests-sagemaker.yml,
openfold3.tests-sagemaker.yml, tensorflow.tests-sagemaker.yml).

Pipeline becomes a thin caller. Cognitive load stays where it belongs:
job orchestration in pipeline files, job bodies in test files.
- Add tensorflow.autorelease-2.20-inference-sagemaker.yml (workflow_dispatch
  only, no schedule). Mirrors training's autorelease shape. Set
  release.environment=gamma and release.public_registry=false in both
  inference configs so the release-gate/release jobs in pipeline-inference
  request the gamma GitHub environment (belt-and-suspenders: no public
  ECR push even if approved).
- Drop DLC_MINOR_VERSION ARG and dlc_minor_version LABEL from both
  Dockerfiles. Matches training's TF 2.21 Dockerfile shape; the minor
  version LABEL always resolved to hardcoded 0 (config never provided
  it), so it was config-that-isn't-really-config.
- Delete orphan .gitkeep in test/tensorflow/integration/inference/
  (directory now has 7 tracked files).
The sagemaker-test on PR #6243 ran for the first time (after 631c772
fixed the silent-skip) and failed at test collection with:

  AttributeError: module 'tensorflow' has no attribute 'constant'

Root cause: build_sample_model.py uses full TF APIs (tf.constant,
tf.function, tf.saved_model.save) to construct the SavedModel tarball
uploaded to S3 for the container to serve. That code runs on the CI
runner, not inside the container. The runner installs
test/tensorflow/integration/inference/requirements.txt only — which
had no tensorflow entry. sagemaker>=3.0.0 pulls a tensorflow namespace
stub (no ops), hence the AttributeError.

The container itself deliberately does NOT ship full tensorflow
(--no-deps on tensorflow-serving-api, matches master TF 2.19 inference
convention). Runtime serving path uses the TFS C++ binary only. Only
the test scaffolding needs full TF, and only on the runner.

Add tensorflow>=2.20,<3.0 to test requirements. Runner gets full TF for
sample-model construction; container is unchanged.
_reusable.sanity-tests.yml fires test_sanity_training.py for any
framework=='tensorflow', which fails on tensorflow inference images
that legitimately don't ship the training-cluster surface (SSH, MPI,
EFA, /opt/ml, DLC_CONTAINER_TYPE=training).

Add the job_type=='training' gate, mirroring the existing Ray
precedent three lines below. TF 2.21 training (job_type=training)
still runs the script; the new TF 2.20 inference (job_type=inference)
correctly skips it, matching peer inference frameworks (openfold3,
vllm_omni, ray inference, huggingface-vllm) that all rely on
universal sanity checks plus their own tests-sagemaker reusables
for real functional validation.
Two fixes that turn CI green:

1. njs dynamic module path (Dockerfile.cpu + Dockerfile.cuda)
   AL2023 nginx compiled with --prefix=/usr/share/nginx resolves
   relative `load_module modules/...` paths against that prefix.
   The njs .so was being copied to /usr/lib64/nginx/modules/ (the
   --modules-path default), which nginx never checks for a relative
   load_module directive. Result: nginx failed at parse, supervisor
   infinite-restart-looped, /ping health check never came up, SM
   endpoint deployment timed out.

   Fix: copy the module to /usr/share/nginx/modules/ where the
   relative directive resolves. nginx.conf.template stays
   byte-for-byte identical to the ported upstream toolkit.

   Discovered via CloudWatch logs from the failed endpoint on
   92ba4bc CI run.

2. pyasn1 CVE bump (cpu/pyproject.toml + cuda/pyproject.toml)
   CVE-2026-59885, CVE-2026-59886 (HIGH, DoS) in pyasn1 0.6.3.
   Fixed in 0.6.4. Add direct pin, mirroring TF 2.21 training's
   solution in docker/tensorflow/2.21/{cpu,cuda}/pyproject.toml.
Fix 1 in commit 2abebaf moved ngx_http_js_module.so to the path
nginx actually reads (/usr/share/nginx/modules/). Now nginx finds
the module, but dlopen() cascades into a missing transitive dep:

  nginx: [emerg] dlopen() ".../ngx_http_js_module.so" failed
  (libxslt.so.1: cannot open shared object file)

The njs module was compiled against libxslt-devel in builder-njs
(added in 21de4f2), so it links against libxslt.so.1. The runtime
image installs nginx but doesn't pull libxslt.

Add libxslt to the runtime dnf install in both Dockerfiles.
Discovered via CloudWatch logs from the ping-health-check failure
on 2abebaf.
Bhanu Teja Goshikonda added 30 commits July 29, 2026 23:11
Override the pipeline default (false) at the autorelease caller so
the ECR CVE scan gates the gamma release path. PR-time pipeline
keeps the platform-standard default.
Adds a small Conv2D SavedModel builder and a GPU-only integration
test that deploys the model and issues a real prediction. Guards
against silent cuDNN ABI drift that ldd + ldconfig -p checks miss.

Model: Conv2D(4,k=3) -> GlobalAveragePooling2D -> Dense(1), 117 params.
Skip mechanism: module-level pytestmark on SM_DEVICE_TYPE != "gpu".
Add Falcon TestClient unit tests for python_service handlers that
aren't reachable from outside a SageMaker MME container:
  - on_delete: unload + reload cycle
  - on_get: loaded model status + 404 for missing
  - traversal-guard: 8 rejection cases (parent path, absolute, NUL,
    dot, dot-dot, hidden prefix, etc.)

Also add a parametrized SageMaker integration test asserting the
traversal guard rejects malicious target_model values end-to-end.
Ruff v0.14.3 (repo-pinned in .pre-commit-config.yaml) auto-formatted two
inference test files added on this branch:

- test_conv_gpu.py: wrap two long-line assert messages onto multiple
  lines; drop the extra blank line between imports and the pytestmark
  block.
- test_mme_dynamic.py: collapse a chained .get() call back onto a
  single line under the 100-char limit.

No behavioral change. Verified locally with ruff 0.14.3 (format --check
and check --select I both pass).
The unit tests in test_python_service_unit.py depended on the
container-only /sagemaker/ lock file path and transitively triggered
gevent monkey-patching of the pytest process, causing an SSL recursion
cascade in every downstream boto3-using test.

Coverage of handler-level MME code paths (on_delete, on_get, traversal
guard) will be re-added as a container-side unit test suite in a
follow-up. The SageMaker integration traversal test in test_mme_dynamic
still covers the end-to-end path.
Wrap the Keras Conv2D model in a tf.Module subclass so the serving
signature and the model variables share a trackable root. The prior
bare tf.function closed over the Keras model via Python scope, which
left dense/bias unbound at invoke time and returned:
  FAILED_PRECONDITION: Could not find variable dense/bias
Mirrors the MultiplierModel pattern used by build_sample_model.
Ruff-format flagged the @tf.function decorator on line 142 of
build_sample_model.py as too long after the ConvSmokeModel rewrite
in 15ff812. Split the input_signature argument onto its own line
to match ruff's formatting.
Customers export Keras models to SavedModel via either
model.export() (Keras 3, recommended) or the legacy
tf.keras.models.save_model(). Parametrize the Conv2D GPU test over
both paths so a regression in TFS 2.20 compat with either path
surfaces in CI rather than in a customer endpoint.

Prior tf.Module + Keras layer wrapper hit auto-uniqueing during
@tf.function trace and produced FAILED_PRECONDITION on serve. The
Keras writers own both signature and variables end-to-end and match
what customers actually run.
Keras 3 (bundled with TF 2.20) removed the save_format='tf' argument
from both tf.keras.models.save_model() and model.save(). Any attempt
to produce a "legacy" SavedModel from within a Keras 3 environment
raises ValueError immediately, so the parametrization tested nothing
that could ever pass.

The remaining model_export parametrization already exercises the same
cuDNN Conv2D forward path end-to-end on GPU, which is what the test
exists to prove. Drop the parametrize/dict indirection and call the
single builder directly.
…opagation

Five defects surfaced by a deep code review of the inference handler:

* GET /models/{name} returned 500 for every loaded model.
  json.dumps(Response) raised TypeError, which the surrounding
  except ValueError did not catch. Parse the body first and widen
  the except.

* The MME model-name traversal guard existed only on the POST
  (load) path. GET and DELETE routes accepted the same path
  component unvalidated. Hoist the guard into a helper and apply
  it uniformly.

* dockerd_entrypoint.sh ran the CMD via 'eval "\$@"', which leaves
  the payload as a child of PID 1 and swallows SIGTERM. Every
  endpoint scale-in was a SIGKILL after Docker's 10s grace, with
  in-flight requests dropped instead of drained. Switch to exec.

* _delete_model raised ProcessLookupError (an OSError subclass) if
  a recorded TFS pid was already gone, aborting on_delete before
  the dict/pickle bookkeeping. The model name was then permanently
  wedged at 409 with no self-healing path. Catch the specific
  subclass and log.

* tensorflowServing.js csv_request used String.replace with string
  arguments, which replace only the first match. Any CSV with a
  leading non-numeric column returned a 200 with wrong tensor
  shape. Use /g regex form.

Extend the CSV content-type test to reach the previously-uncovered
branch.
Collapse the multi-line json.dumps call in _reject_bad_model_name to
match ruff-format's line-length rules; ruff-check clean afterward.
Two Dockerfile constructs silently masked what they claimed to do:

* uv sync did not include --extra sagemaker, so the pandas /
  scikit-learn / cloudpickle packages declared in the pyproject
  optional-dependencies (and documented as available for customer
  inference handlers) never landed in the runtime venv. A customer
  handler with `import pandas` failed at first invocation. Mirror
  the pattern already used by the TF 2.21 training Dockerfile.

* /opt/venv/bin/uv did not exist — uv was staged only into
  builder-base, and only /opt/venv is copied out to the runtime
  stage. Every build silently took the `|| pip install` fallback.
  Stage uv into the sagemaker stage that uses it (same :latest
  reference as builder-base and training), drop the fallback, and
  let a real failure surface at build time.
Four test-layer defects surfaced by a deep code review:

* deploy_endpoint returned the tuple only after wait_for_status
  succeeded, so a Failed endpoint never registered for cleanup
  and leaked continuously. Move registration into the fixture
  before any AWS create call so a mid-flight deploy failure still
  tears down.

* test_nginx_env_vars supplied no code_files, so _use_gunicorn
  stayed False and three of the four SAGEMAKER_GUNICORN_* env
  vars were inert despite the docstring claiming coverage. Add a
  minimal passthrough inference.py so gunicorn actually starts
  and assert a gunicorn-served marker key round-trips.

* test_conv_gpu fed an all-zeros payload; Keras zero-init biases
  make the output deterministically 0.0 regardless of kernel
  weights, so a stubbed-zero response would pass. Feed 1.0 and
  assert non-zero.

* MME target_model traversal is already exercised end-to-end via
  the SageMaker InvokeEndpoint API in test_mme_dynamic.py; add a
  README-coverage-gaps.md documenting the residual GET/DELETE
  coverage gap (routes reachable only from inside the container)
  and what a container-level harness would need to look like to
  close it.
* dockerfmt rewrote the two-line RUN continuation indent from 4 spaces
  to 2 spaces in both inference Dockerfiles.
* mdformat rewrote the numbered list in README-coverage-gaps.md to all
  start with "1." (renumbered on render).
* H-1 — test_conv_gpu had a ~6% flake rate on all-ones + Glorot init:
  P(all 4 Conv2D filters have non-positive weight sum) = 1/16. Pin
  bias_initializer to a positive constant on both Conv2D and Dense
  layers so pre-activation is deterministically positive. Removes a
  flake whose failure message is indistinguishable from the real cuDNN
  regression it guards.

* H-3 — add /usr/bin compat symlinks for tensorflow_model_server and
  tf_serving_entrypoint.sh in both Dockerfile.cpu and Dockerfile.cuda.
  Master's canary/ECS/EKS harnesses hardcode /usr/bin/; v2 image
  installs to /usr/local/bin/. Symlinks eliminate a latent hard-break
  at GA promotion, zero risk today.

* H-5 — extend test_content_types.py with a quoted, comma-containing
  CSV case that reaches the needs_quotes=true branch. Previously the
  fix to tensorflowServing.js:214 (non-global replace -> /g regex)
  shipped with no test coverage.

* H-6 — narrow test_mme_traversal_rejected's pytest.raises to
  (ClientError, ParamValidationError) and assert unconditionally. Was
  pytest.raises(Exception) with the assertion inside a ClientError
  isinstance guard, so a ParamValidationError satisfied the test
  without asserting anything.

* M-2 — remove stale comment in Dockerfile.cuda that still describes
  the pre-fix eval-based entrypoint behavior; the current entrypoint
  uses exec so signals reach TFS as PID 1.

* M-11 — tighten test_mme_dynamic.py unknown-model status band from
  400..600 to 400..500. Docstring says 4xx, band accepted 5xx.
* H-2 — enumerate cuDNN 9 sub-libraries (_ops, _cnn, _adv, _graph,
  _engines_precompiled, _engines_runtime_compiled, _heuristics) and
  assert each is present on disk and resolvable via ldconfig. The
  existing libcudnn*-glob and libcudnn.so substring checks would pass
  for a build shipping only the dispatcher stub, which is the exact
  failure mode that let a prior training-canary cuDNN regression ship
  past ldd + libcudnn glob checks.

* H-4 — pin the TFS-2.20-serves-TF-2.21 forward-compat boundary.
  DecodeJxl is the one net-new op in TF 2.21 that TFS 2.20 does not
  know; failure signature is uniquely nasty (status reports AVAILABLE,
  first predict returns 4xx Op type not registered), so a health-check
  rollout gate would go green with the customer taking the error.
  Document the boundary in code, not just prose — a future TFS upgrade
  that gains DecodeJxl support flips this test green automatically.
dockerfmt wants 2-space indent on `&&`-continuation lines. The H-3
symlink block used 1 space, tripping pre-commit. Match the file's
established 2-space style.
…l test

Two issues surfaced on the c176746 CI run:

* Sanity test asserted `libcudnn_heuristics.so.9` (plural), but upstream
  cuDNN 9 ships `libcudnn_heuristic.so.9` (singular). Typo in the
  sub-library list added by 7df7dff. One-char fix.

* The DecodeJxl boundary test could not reliably distinguish "TFS
  rejected the op" from "SM returned an empty response body for another
  reason": when the test host has TF 2.21, the exported SavedModel
  reaches the endpoint in a form TFS 2.20 accepts (op inlined or
  optimized away during export). The test asserted a 4xx with an
  op-not-registered marker in the body and got a 4xx with empty body —
  which reveals the test is not measuring what its docstring claims.

  Delete the test and document the DecodeJxl boundary in
  README-coverage-gaps.md instead. A proper boundary test needs a
  direct-to-TFS harness (bypassing SM's response reshaping) or a
  pre-built SavedModel artifact that guarantees the op survives export
  — both out of scope for this PR.
mdformat re-wrapped the "First predict" bullet in the DecodeJxl
section to one line. Apply the auto-fix so pre-commit is green.
* B-1 (test/tensorflow/integration/inference/test_conv_gpu.py + resources/
  build_sample_model.py): the conv test had a 100% false-negative rate
  on cuDNN kernel bypass. With Dense bias pinned to 1.0, a fully dead
  conv (feature map = 0) still produced Dense output = 0 + 1.0 = 1.0,
  which passed `scalar != 0.0`. Replace random-init with closed-form
  constant init: Conv2D kernel=1.0/bias=0.0, Dense kernel=1.0/bias=0.0,
  all-ones input. Deterministic output is 108.0 (27+27+27+27 * 1.0).
  A dead conv now yields 0.0 and fails loudly. No flake because
  everything is a constant.

* B-2 (.github/workflows/tensorflow.pr-2.20-inference-{cpu,cuda}.yml):
  test-only PR edits skipped every SageMaker integration test and
  reported the lane green. Chain: test files not in build-change
  filter -> build job skipped -> image-uri empty -> check-image-exists
  returns false (gracefully) -> sagemaker-test skipped -> parent job
  succeeds. Add test/tensorflow/integration/inference/** to
  build-change so an integration test edit forces a rebuild.

* B-3 (test/sanity/scripts/test_sanity_tf_inference.py): my cuDNN
  sub-library list omitted two libraries the 9.24 wheel actually
  ships: libcudnn_ext.so.9 (dlopen'd by name by the libcudnn.so.9
  dispatcher, new in the 9.24 line) and libcudnn_engines_tensor_ir.so.9
  (transitive via DT_NEEDED on libcudnn_graph). The cp glob copies
  them so the image is fine today, but the guard cannot detect a
  regression that dropped either. Add both.

* B-4 (scripts/docker/tensorflow/inference/sagemaker/python_service.py):
  MME + batching (SAGEMAKER_MULTI_MODEL=true and
  SAGEMAKER_TFS_ENABLE_BATCHING=true) failed every POST /models with
  a 500. Root cause: tfs_config_file's parent dir is created via
  os.makedirs, but batching_config_file's parent dir is not.
  create_batching_config does a bare open() which raises
  FileNotFoundError (OSError errno=2), which the outer handler only
  maps to 507 for errno=12 (ENOMEM), so it fell through to 500.
  Inherited from master. Fix: mirror the tfs-config makedirs.
* PR trigger paths: add tensorflow.tests-sagemaker-inference.yml
  to both cpu and cuda pr workflows. Without it, a PR editing only
  the test workflow triggers zero CI checks and is mergeable with
  no signal. Training already lists its test workflows both.

* SM_DEVICE_TYPE fail-closed: conftest.py and test_conv_gpu.py both
  defaulted to "cpu" on missing env var. If yq ever prints "null"
  for a missing config key, the CUDA lane would silently test on a
  CPU instance and skip the GPU-only conv test — green. Fail loudly
  on missing / invalid values.

* Drop /opt/ml/code from Dockerfile mkdir: serve.py gates the MME
  universal-script S3 download on `not os.path.exists("/opt/ml/code")`.
  Pre-creating the dir made exists() true, silently disabling the
  download for customers using SAGEMAKER_MULTI_MODEL_UNIVERSAL_BUCKET.
  Master creates none of /opt/ml/*.

* Strip internal-artifact provenance from public-repo comments:
  handoff-section refs and audit-finding IDs in pyproject/sanity/
  conftest replaced with the technical constraint stated directly.
  Public PR numbers (#6418, #6243, #6107) retained.

* Sanity: add ctypes.CDLL per cuDNN sub-lib (mirrors training file
  pattern). Catches truncated / zero-byte / wrong-arch / ABI-drifted
  .so files that pass glob + ldconfig -p.

* Sanity: assert EXPECTED_TFS_VERSION in tensorflow_model_server
  --version. Previously accepted any "TensorFlow ModelServer"
  string — a build that silently shipped 2.19 or 2.21 passed.

* Delete test_mme_traversal_rejected — botocore does not enforce
  pattern-validation on TargetModel, so ParamValidationError is
  dead code, and the ClientError assertion is byte-identical to
  test_mme_target_model_not_found. Reverting the container guard
  leaves this test green. Update README-coverage-gaps.md.

* Delete test_csv_content_type_quoted_string_with_embedded_commas
  — payload starts with " so needs_quotes evaluates false and the
  /g branch this test claimed to cover never executes.
HIGH — out-of-bounds heap read in aiohttp's C response parser pre-3.14.3.
Bump the direct pin from >=3.14.0 to >=3.14.3 in both cpu and cuda
pyproject.toml, re-lock. Only aiohttp version changed in both lockfiles
(3.14.1 -> 3.14.3, 45 CPU / 48 CUDA packages resolved unchanged
otherwise).

aiohttp is pulled transitively via aiobotocore. The pin exists to force
the resolver to a patched line; keeping the pin below the CVE fix
version defeats the purpose.
serve.py:_enable_per_process_gpu_memory_fraction hardcodes
/usr/bin/nvidia-smi (inherited from master). On a GPU host the NVIDIA
container runtime injects nvidia-smi into the container at that path.
A base-image bump (e.g. nvidia/cuda 12.9.1 -> 12.10 -> 13.0) that moves
the injection path would silently disable the memory-fraction gate,
and MME customers running SAGEMAKER_TFS_INSTANCE_COUNT>1 would hit OOM
at first inference with no log signal.

Assert /usr/bin/nvidia-smi exists under @gpu_only so CI catches this
class of regression on every push. Cost is a single os.path.exists
call at sanity time.
Comment-only sweep. Trims long design-decision explanations, historical
narration, and multi-paragraph docstrings down to crisp one-liners.
Keeps load-bearing facts (why a fix exists, what a subtle constraint
means) but cuts narration of prior review rounds, restated code
behavior, and rejected alternatives.

Zero code changes.
The sanity container is launched via `docker run -d -it --entrypoint
/bin/bash` in _reusable.sanity-tests.yml — no `--gpus all` flag. The
NVIDIA container runtime only injects /usr/bin/nvidia-smi when the
container is launched with GPU passthrough, so the assertion fails
on every CUDA image regardless of image content.

@gpu_only skips based on EXPECTED_DEVICE env var, not on actual GPU
availability in the sanity container.

The check needs to run against a real GPU-passthrough container
(devbox with `docker run --gpus all <image> ls -l /usr/bin/nvidia-smi`
or an integration test that deploys to a real SM GPU endpoint).
Removing the sanity assertion; the actual verification is a separate
follow-up.
* tfs_utils split("=", 1): base64-padded custom-attribute values raised
  ValueError uncaught, surfacing as opaque 500.

* python_service pickle write: made atomic (tmp + fsync + rename) and
  wrapped load in try/except (EOFError, UnpicklingError) resetting to
  empty. Previously an OOM SIGKILL mid-write bricked all MME routes
  with permanent 500 while /ping stayed 200.

* tfs_utils.make_tfs_uri: validate tfs-model-version against ^\d+$ and
  tfs-method against {predict, classify, regress}. Prevents unvalidated
  interpolation into the TFS REST URI.

* multi_model_utils.lock: replaced blocking fcntl.lockf with non-blocking
  retry + gevent-yielding sleep. Blocking lockf parked the entire hub
  including /ping under contention with SAGEMAKER_GUNICORN_WORKERS>=2.
  Dropped the inherited-master 1s sleep before LOCK_UN.

* python_service load failure: pop the ghost dict entry and re-upload
  status. Without this, setdefault().append() on the failure branch
  left a permanent 409 "already loaded" for that model name.

* tests-sagemaker-inference.yml: device-type now required, no default.
  Fail closed on config wiring bugs instead of silently deploying the
  CUDA lane to ml.c5.xlarge.

* pr-2.20-inference-{cpu,cuda}.yml: added test/sanity/** to build-change.
  Sanity edits now trigger a rebuild instead of silently skipping.
…exec, cuDNN RNN

* test_mme_concurrency: fires ThreadPoolExecutor(8) invokes across 3
  distinct-multiplier MME models under SAGEMAKER_GUNICORN_WORKERS=2.
  Asserts each returns its own model's arithmetic. Guards against
  pickle-write atomicity regressions, fcntl.lockf stalls under gevent,
  and ghost-entry-on-failed-load bugs.

* test_sanity_tf_inference: asserts entrypoint scripts use exec (not
  eval) form and contain at least one `exec <cmd>` line — covers
  /usr/local/bin/dockerd_entrypoint.sh, /usr/local/bin/tf_serving_entrypoint.sh,
  and /sagemaker/serve. A regression to eval or a subshell silently
  breaks SIGTERM propagation and turns every SM scale-in into a
  SIGKILL after 10s with in-flight requests dropped.

* test_lstm_gpu: LSTM(units=1) with constant-init weights + all-ones
  input, invoked twice. Asserts finite + non-zero + deterministic
  across invokes. Exercises libcudnn_adv.so.9 (RNN library asserted
  present at sanity but never actually loaded by any request until now);
  complements test_conv_gpu.py which covers libcudnn_cnn / libcudnn_ops.

Fixture pattern: mirrors existing test_conv_gpu.py — SM_DEVICE_TYPE
gate at module scope, deploy_endpoint / cleanup_endpoint reuse, no
hardcoded instance types.
The file cataloged residual test-coverage gaps as review-round narrative.
It duplicated information now captured in commit messages and the PR body,
and its content included branch-local SHAs that won't survive a squash
merge and framing (e.g., "past 24 green CI checks") that reads as
internal review artifacts. Drop it.
test_lstm_gpu_predict fails deterministically with:
  UNKNOWN: JIT compilation failed
  node: sequential/lstm/while/lstm_cell/Sigmoid

Same class of forward-compat boundary as tf.raw_ops.DecodeJxl: TF 2.21's
Keras 3 LSTM export produces a functional graph with a while_loop that
TFS 2.20's XLA/JIT autotuner cannot compile. The test exposed a real
customer-facing gap but the fix belongs to TFS 2.21+ or a downstream
XLA config change — out of scope for this PR.

Also removes _build_lstm_sequential + build_lstm_sample_model helpers
from build_sample_model.py (no remaining callers).

Customer guidance until TFS 2.21+ ships: LSTM / GRU models require
either CPU inference or a future TFS version with the JIT path fixed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant