Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 83 additions & 13 deletions qa/L0_lifecycle/lifecycle_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,12 @@ def test_dynamic_model_load_unload(self):
# load. Make sure that it has a status and is ready.
try:
shutil.copytree(libtorch_name, "models/" + libtorch_name)
time.sleep(5) # wait for model to load
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
True,
"1",
)
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
grpcclient.InferenceServerClient("localhost:8001", verbose=True),
Expand Down Expand Up @@ -635,7 +640,12 @@ def test_dynamic_model_load_unload(self):
# time to unload. Make sure that it is no longer available.
try:
shutil.rmtree("models/" + libtorch_name)
time.sleep(5) # wait for model to unload
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
False,
"1",
)
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
grpcclient.InferenceServerClient("localhost:8001", verbose=True),
Expand Down Expand Up @@ -675,7 +685,12 @@ def test_dynamic_model_load_unload(self):
# Add back the same model. The status/stats should be reset.
try:
shutil.copytree(libtorch_name, "models/" + libtorch_name)
time.sleep(5) # wait for model to load
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
True,
"1",
)
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
grpcclient.InferenceServerClient("localhost:8001", verbose=True),
Expand Down Expand Up @@ -718,7 +733,12 @@ def test_dynamic_model_load_unload(self):
# time to unload. Make sure that it is unavailable.
try:
shutil.rmtree("models/" + onnx_name)
time.sleep(5) # wait for model to unload
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
onnx_name,
False,
"1",
)
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
grpcclient.InferenceServerClient("localhost:8001", verbose=True),
Expand Down Expand Up @@ -929,7 +949,12 @@ def test_dynamic_version_load_unload(self):
# unload. Make sure that it is unavailable.
try:
shutil.rmtree("models/" + libtorch_name + "/1")
time.sleep(5) # wait for version to unload
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
False,
"1",
)
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
grpcclient.InferenceServerClient("localhost:8001", verbose=True),
Expand Down Expand Up @@ -969,7 +994,12 @@ def test_dynamic_version_load_unload(self):
shutil.copytree(
"models/" + libtorch_name + "/2", "models/" + libtorch_name + "/7"
)
time.sleep(5) # wait for version to load
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
True,
"7",
)
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
grpcclient.InferenceServerClient("localhost:8001", verbose=True),
Expand Down Expand Up @@ -1090,7 +1120,13 @@ def test_dynamic_model_modify(self):
"models/" + model_name + "/config.pbtxt",
)

time.sleep(5) # wait for models to reload
for model_name in models:
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
model_name,
True,
str(version),
)
Comment on lines +1124 to +1129

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Already-ready state bypasses reload

When the configuration changes between repository polling cycles, version 3 remains ready under the old configuration, so wait_for_model_state(..., True, "3") returns immediately and the subsequent label and version-policy checks race the reload, causing intermittent test failures. The same issue applies to the waits after the second configuration change and the configuration deletion.

for model_name in models:
for model_name, model_shape in zip(models_base, models_shape):
try:
Expand Down Expand Up @@ -1119,7 +1155,13 @@ def test_dynamic_model_modify(self):
"config.pbtxt." + base_name, "models/" + model_name + "/config.pbtxt"
)

time.sleep(5) # wait for models to reload
for model_name in models:
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
model_name,
True,
"3",
)
for model_name in models:
try:
for triton_client in (
Expand Down Expand Up @@ -1217,7 +1259,13 @@ def test_dynamic_file_delete(self):
for model_name in models:
os.remove("models/" + model_name + "/config.pbtxt")

time.sleep(5) # wait for models to reload
for model_name in models:
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
model_name,
True,
"3",
)
for model_name in models:
try:
for triton_client in (
Expand Down Expand Up @@ -1286,7 +1334,12 @@ def test_multiple_model_repository_polling(self):
# Add the libtorch to the second model repository, should cause
# it to be unloaded due to duplication
shutil.copytree(libtorch_name, "models_0/" + libtorch_name)
time.sleep(5) # wait for models to reload
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
False,
"1",
)
try:
for triton_client in (
httpclient.InferenceServerClient("localhost:8000", verbose=True),
Expand All @@ -1306,7 +1359,12 @@ def test_multiple_model_repository_polling(self):
# properly. In the second model repository libtorch should
# have versions 1 and 3.
shutil.rmtree("models/" + libtorch_name)
time.sleep(5) # wait for model to unload
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
libtorch_name,
True,
"1",
)
self._infer_success_models(
["libtorch", "openvino", "onnx"], (1, 3), model_shape
)
Expand Down Expand Up @@ -3441,7 +3499,13 @@ def test_add_custom_config(self):
"models/" + model_name + "/configs/custom.pbtxt",
)

time.sleep(5) # wait for models to reload
for model_name in models:
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
model_name,
True,
"2",
)
for model_name in models:
try:
for triton_client in (
Expand Down Expand Up @@ -3483,7 +3547,13 @@ def test_delete_custom_config(self):
for model_name in models:
os.remove("models/" + model_name + "/configs/custom.pbtxt")

time.sleep(5) # wait for models to reload
for model_name in models:
tu.wait_for_model_state(
httpclient.InferenceServerClient("localhost:8000", verbose=True),
model_name,
True,
"1",
)
for model_name in models:
try:
for triton_client in (
Expand Down
43 changes: 43 additions & 0 deletions qa/common/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import json
import os
import time
import unittest

import ml_dtypes
Expand Down Expand Up @@ -79,6 +80,48 @@ def wait_for_stable_rss(server, rss_tolerance_bytes=0.1 * MIB, stable_threshold=
return


def wait_for_model_state(
triton_client,
model_name,
expected_ready,
model_version="",
timeout_sec=30,
interval_sec=1,
):
"""Poll ``is_model_ready`` until it matches *expected_ready* or timeout.

Replaces blind ``time.sleep`` calls that wait for model load/unload
with a deterministic polling loop, reducing flaky failures under CI
resource contention.

Args:
triton_client: A Triton ``InferenceServerClient`` (HTTP or gRPC).
model_name: Name of the model to check.
expected_ready: ``True`` to wait for the model to become ready,
``False`` to wait for it to become *not* ready.
model_version: Version string (empty string for default/latest).
timeout_sec: Maximum seconds to wait before raising.
interval_sec: Seconds between consecutive polls.

Raises:
AssertionError: If the model does not reach the expected state
within *timeout_sec*.
"""
deadline = time.time() + timeout_sec
while time.time() < deadline:
if triton_client.is_model_ready(model_name, model_version) == expected_ready:
return
time.sleep(interval_sec)
actual = triton_client.is_model_ready(model_name, model_version)
if actual != expected_ready:
raise AssertionError(
"Model '{}' version '{}' did not reach expected ready={} "
"within {}s (actual ready={})".format(
model_name, model_version, expected_ready, timeout_sec, actual
)
)


def shape_element_count(shape):
cnt = 0
for d in shape:
Expand Down