diff --git a/components/src/dynamo/common/multimodal/audio_loader.py b/components/src/dynamo/common/multimodal/audio_loader.py index 15bb17b78930..79d4c3df631b 100644 --- a/components/src/dynamo/common/multimodal/audio_loader.py +++ b/components/src/dynamo/common/multimodal/audio_loader.py @@ -21,6 +21,8 @@ from dynamo.common.utils import nvtx_utils as _nvtx from dynamo.common.utils.runtime import run_async +from dynamo.common.http.media_reference import max_media_bytes # isort: skip + logger = logging.getLogger(__name__) # Constants for multimodal data variants @@ -128,7 +130,10 @@ async def _load_audio_with_vllm(self, audio_url: str) -> tuple[np.ndarray, float # data: and file:// never touch the network, so vLLM can handle them. if urlparse(normalized_url).scheme in ("http", "https"): content = await fetch_bytes( - normalized_url, self._http_timeout, policy=self._url_policy + normalized_url, + self._http_timeout, + policy=self._url_policy, + max_bytes=max_media_bytes(), ) return await asyncio.to_thread(media_io.load_bytes, content) diff --git a/components/src/dynamo/common/multimodal/image_loader.py b/components/src/dynamo/common/multimodal/image_loader.py index 1cc22929d18c..bc03dd35733e 100644 --- a/components/src/dynamo/common/multimodal/image_loader.py +++ b/components/src/dynamo/common/multimodal/image_loader.py @@ -23,6 +23,7 @@ HttpTimeoutError, fetch_bytes, ) +from ..http.media_reference import max_media_bytes from ..http.url_validator import ( UrlValidationError, UrlValidationPolicy, @@ -71,6 +72,7 @@ def __init__( http_timeout: float = 30.0, enable_frontend_decoding: bool = False, url_policy: UrlValidationPolicy | None = None, + max_bytes: int | None = None, ): """ Initialize the ImageLoader with caching, HTTP settings, and optional NIXL config for @@ -85,9 +87,12 @@ def __init__( decoded images directly from frontend memory, bypassing standard network transport. Defaults to False. url_policy: Policy for validating URLs. Defaults to UrlValidationPolicy.from_env(). + max_bytes: Maximum remote image size in bytes. When omitted, resolve + DYN_MM_MAX_FILE_SIZE_MB for each request. """ self._http_timeout = http_timeout self._cache_size = cache_size + self._configured_max_bytes = max_bytes self._image_cache: OrderedDict[str, Image.Image] = OrderedDict() self._inflight: dict[str, asyncio.Task[Image.Image]] = {} self._enable_frontend_decoding = enable_frontend_decoding @@ -100,6 +105,11 @@ def __init__( self._nixl_connector.initialize ) # Synchronously wait for async init + def _max_bytes(self) -> int: + if self._configured_max_bytes is not None: + return self._configured_max_bytes + return max_media_bytes() + @staticmethod def _open_image_sync(image_data: BytesIO) -> Image.Image: """Open, validate, and decode an image from raw bytes. Runs in a thread.""" @@ -135,7 +145,10 @@ async def _fetch_and_process(self, image_url: str) -> Image.Image: try: with _nvtx.annotate("mm:img:http_fetch", color="lime"): content = await fetch_bytes( - image_url, self._http_timeout, policy=self._url_policy + image_url, + self._http_timeout, + policy=self._url_policy, + max_bytes=self._max_bytes(), ) if not content: raise ValueError("Empty response content from image URL") diff --git a/components/src/dynamo/common/multimodal/video_loader.py b/components/src/dynamo/common/multimodal/video_loader.py index 042bd24b8f93..4c40cb75cb38 100644 --- a/components/src/dynamo/common/multimodal/video_loader.py +++ b/components/src/dynamo/common/multimodal/video_loader.py @@ -44,6 +44,8 @@ ) from dynamo.common.utils.runtime import run_async +from dynamo.common.http.media_reference import max_media_bytes # isort: skip + logger = logging.getLogger(__name__) @@ -171,7 +173,10 @@ async def _load_video_with_vllm( # data: and file:// never touch the network, so vLLM can handle them. if urlparse(normalized_url).scheme in ("http", "https"): content = await fetch_bytes( - normalized_url, self._http_timeout, policy=self._url_policy + normalized_url, + self._http_timeout, + policy=self._url_policy, + max_bytes=max_media_bytes(), ) return await self._decode_video_bytes(content, media_io) diff --git a/components/src/dynamo/common/tests/multimodal/test_audio_loader.py b/components/src/dynamo/common/tests/multimodal/test_audio_loader.py index a962fa76a0eb..d8a8cf9ac946 100644 --- a/components/src/dynamo/common/tests/multimodal/test_audio_loader.py +++ b/components/src/dynamo/common/tests/multimodal/test_audio_loader.py @@ -13,6 +13,8 @@ from dynamo.common.multimodal.codec_errors import MissingMediaDecoderError from dynamo.common.utils.install_media_decoders import VALIDATED_SPECS +from dynamo.common.http.media_reference import DYN_MM_MAX_FILE_SIZE_MB # isort: skip + pytestmark = [ pytest.mark.unit, pytest.mark.gpu_0, @@ -94,6 +96,25 @@ async def test_load_audio_rejects_empty_waveform(): await loader.load_audio("https://example.com/empty.wav") +@pytest.mark.asyncio +async def test_http_fetch_honors_configured_media_limit(monkeypatch): + monkeypatch.setenv(DYN_MM_MAX_FILE_SIZE_MB, "1") + loader = AudioLoader(url_policy=_permissive_http_policy()) + waveform = np.zeros(8, dtype=np.float32) + + class _MediaIO: + def load_bytes(self, content): + return waveform, 16000.0 + + mock_fetch = AsyncMock(return_value=b"audio") + monkeypatch.setattr(audio_loader_module, "fetch_bytes", mock_fetch) + monkeypatch.setattr(loader, "_create_vllm_audio_io", lambda: _MediaIO()) + + await loader._load_audio_with_vllm("https://example.com/limited.wav") + + assert mock_fetch.await_args.kwargs["max_bytes"] == 1024 * 1024 + + @pytest.mark.asyncio async def test_load_audio_batch_uses_url_loader(): loader = AudioLoader() diff --git a/components/src/dynamo/common/tests/multimodal/test_image_loader.py b/components/src/dynamo/common/tests/multimodal/test_image_loader.py index 647e43b0a401..a92d02cb50ab 100644 --- a/components/src/dynamo/common/tests/multimodal/test_image_loader.py +++ b/components/src/dynamo/common/tests/multimodal/test_image_loader.py @@ -25,6 +25,7 @@ from PIL import Image from dynamo.common.http import HttpConnectionError, HttpStatusError, HttpTimeoutError +from dynamo.common.http.media_reference import DYN_MM_MAX_FILE_SIZE_MB from dynamo.common.http.url_validator import UrlValidationError, UrlValidationPolicy from dynamo.common.multimodal.image_loader import URL_VARIANT_KEY, ImageLoader @@ -73,7 +74,7 @@ def _mock_fetch_bytes( side_effect: If set, the mock raises this exception instead of returning. """ - async def _fetch(url, timeout, *, policy=None): + async def _fetch(url, timeout, *, policy=None, max_bytes=None): if delay > 0: await asyncio.sleep(delay) if side_effect is not None: @@ -207,6 +208,34 @@ async def test_http_timeout_raises_408(loader: ImageLoader) -> None: assert "https://example.com/img.png" in exc_info.value.url +async def test_http_fetch_honors_configured_media_limit( + loader: ImageLoader, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv(DYN_MM_MAX_FILE_SIZE_MB, "1") + mock_fetch = _mock_fetch_bytes() + + with patch(_FETCH_BYTES_PATH, mock_fetch): + await loader.load_image("https://example.com/limited.png") + + assert mock_fetch.await_args.kwargs["max_bytes"] == 1024 * 1024 + + +async def test_explicit_media_limit_overrides_environment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv(DYN_MM_MAX_FILE_SIZE_MB, "1") + loader = ImageLoader( + max_bytes=2 * 1024 * 1024, + url_policy=_permissive_policy(), + ) + mock_fetch = _mock_fetch_bytes() + + with patch(_FETCH_BYTES_PATH, mock_fetch): + await loader.load_image("https://example.com/explicit-limit.png") + + assert mock_fetch.await_args.kwargs["max_bytes"] == 2 * 1024 * 1024 + + async def test_http_connection_error_raises_400(loader: ImageLoader) -> None: """An unreachable user-supplied URL is a client error, so the frontend must surface 400 instead of an opaque 500.""" diff --git a/components/src/dynamo/common/tests/multimodal/test_video_loader.py b/components/src/dynamo/common/tests/multimodal/test_video_loader.py index b6b427c93c49..d7a4bd42d175 100644 --- a/components/src/dynamo/common/tests/multimodal/test_video_loader.py +++ b/components/src/dynamo/common/tests/multimodal/test_video_loader.py @@ -15,6 +15,8 @@ from dynamo.common.multimodal.video_loader import VideoLoader from dynamo.common.utils.install_media_decoders import VALIDATED_SPECS +from dynamo.common.http.media_reference import DYN_MM_MAX_FILE_SIZE_MB # isort: skip + pytestmark = [ pytest.mark.unit, pytest.mark.pre_merge, @@ -75,6 +77,25 @@ async def test_load_video_uses_vllm_media_connector(): assert loaded_metadata == metadata +@pytest.mark.asyncio +async def test_http_fetch_honors_configured_media_limit(monkeypatch): + monkeypatch.setenv(DYN_MM_MAX_FILE_SIZE_MB, "1") + loader = VideoLoader( + url_policy=UrlValidationPolicy(allow_http=True, allow_private_ips=True) + ) + frames = np.zeros((1, 2, 2, 3), dtype=np.uint8) + metadata = {"fps": 1.0, "frames_indices": [0], "total_num_frames": 1} + mock_fetch = AsyncMock(return_value=b"video") + mock_decode = AsyncMock(return_value=(frames, metadata)) + monkeypatch.setattr(video_loader_module, "fetch_bytes", mock_fetch) + monkeypatch.setattr(loader, "_create_vllm_video_io", lambda kwargs=None: object()) + monkeypatch.setattr(loader, "_decode_video_bytes", mock_decode) + + await loader._load_video_with_vllm("https://example.com/limited.mp4") + + assert mock_fetch.await_args.kwargs["max_bytes"] == 1024 * 1024 + + @pytest.mark.asyncio async def test_load_video_batch_uses_url_loader(): loader = VideoLoader() diff --git a/components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py b/components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py index 42209b9edd90..dafafbccef49 100644 --- a/components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py +++ b/components/src/dynamo/sglang/request_handlers/multimodal/encode_worker_handler.py @@ -24,6 +24,7 @@ from dynamo._core import Client, Context from dynamo.common.http import fetch_bytes +from dynamo.common.http.media_reference import max_media_bytes from dynamo.common.http.url_validator import UrlValidationPolicy, validate_media_url from dynamo.common.memory.multimodal_embedding_cache_manager import ( CachedEmbedding, @@ -614,7 +615,12 @@ async def _maybe_nvdec_decoder(self, url: str) -> Optional[Any]: normalized = await validate_media_url(url, self._url_policy) scheme = urlparse(normalized).scheme if scheme in ("http", "https"): - content = await fetch_bytes(normalized, 30.0, policy=self._url_policy) + content = await fetch_bytes( + normalized, + 30.0, + policy=self._url_policy, + max_bytes=max_media_bytes(), + ) elif is_local_media_url(normalized): content = await read_local_media_bytes(normalized, self._url_policy) else: @@ -680,8 +686,9 @@ async def _build_encode_inputs( so SGLang does not re-download what we already validated and hold), or the original URL string when nothing was fetched. Non-video modalities and decoded inputs are returned unchanged. When - NVDEC is disabled or ineligible the URLs are returned policy-validated - and normalized, since SGLang fetches them with its own session. + NVDEC is disabled or ineligible, remote URLs are fetched under Dynamo's + policy and size limit and passed to SGLang as bytes; other URLs remain + policy-validated and normalized. Called from both the cached and uncached encode paths. The embedding cache is disabled by default, so routing this only through the cached @@ -691,9 +698,8 @@ async def _build_encode_inputs( return media_inputs if not self._nvdec_video_enabled(): # NVDEC off (CPU image, DYN_DISABLE_NVDEC, or a gated model type): - # these URLs go straight to SGLang's software path, which fetches - # them with its own session and never consults our url policy. Run - # the policy here so a source we would refuse is refused before + # these inputs go straight to SGLang's software path. Validate them + # here so a source we would refuse is refused before # SGLang can reach it -- and before we answer with anything about # this deployment, since a request we reject is not the place to # report which decoders are installed. @@ -713,7 +719,20 @@ async def _build_encode_inputs( and not _software_video_decoder_imports() ): raise video_decoder_missing("sglang", "decord2", "decord", None) - return validated + fetched_inputs: list[Any] = [] + for media_input in validated: + if isinstance(media_input, str) and urlparse(media_input).scheme in ( + "http", + "https", + ): + media_input = await fetch_bytes( + media_input, + 30.0, + policy=self._url_policy, + max_bytes=max_media_bytes(), + ) + fetched_inputs.append(media_input) + return fetched_inputs encode_inputs: list[Any] = [] for media_input in media_inputs: if not isinstance(media_input, str): diff --git a/components/src/dynamo/sglang/tests/test_sglang_multimodal_embedding_cache.py b/components/src/dynamo/sglang/tests/test_sglang_multimodal_embedding_cache.py index 4d53e0a7d1bc..5612bb555183 100644 --- a/components/src/dynamo/sglang/tests/test_sglang_multimodal_embedding_cache.py +++ b/components/src/dynamo/sglang/tests/test_sglang_multimodal_embedding_cache.py @@ -18,6 +18,7 @@ HttpStatusError, HttpTimeoutError, ) +from dynamo.common.http.media_reference import DYN_MM_MAX_FILE_SIZE_MB from dynamo.common.http.url_validator import ( UrlValidationError, UrlValidationPolicy, @@ -309,11 +310,15 @@ async def test_encode_with_cache_reencodes_only_unkeyed_items( @pytest.mark.asyncio async def test_video_requests_reuse_cached_embeddings( cache_handler: MultimodalEncodeWorkerHandler, + monkeypatch, ) -> None: """Second identical video request should reuse cached embeddings.""" video_url = "https://example.com/clip.mp4" video_token_id = cache_handler.video_token_id + video_bytes = b"video-bytes" + fetch = AsyncMock(return_value=video_bytes) + monkeypatch.setattr(f"{_HANDLER_MOD}.fetch_bytes", fetch) cache_handler.encoder.encode_mock.return_value = ( torch.tensor([2, 3, 4]), @@ -370,8 +375,9 @@ async def _responses(): outputs_second.append(item) cache_handler.encoder.encode_mock.assert_awaited_once_with( - [video_url], Modality.VIDEO + [video_bytes], Modality.VIDEO ) + fetch.assert_awaited_once() assert outputs == [{"token_ids": [7]}] assert outputs_second == [{"token_ids": [7]}] @@ -400,9 +406,13 @@ async def _responses(): @pytest.mark.asyncio async def test_video_request_skips_cache_key_when_cache_is_disabled( cache_handler: MultimodalEncodeWorkerHandler, + monkeypatch, ) -> None: video_url = "https://example.com/clip.mp4" video_token_id = cache_handler.video_token_id + video_bytes = b"video-bytes" + fetch = AsyncMock(return_value=video_bytes) + monkeypatch.setattr(f"{_HANDLER_MOD}.fetch_bytes", fetch) cache_handler._embedding_cache = None cache_handler._image_loader = None cache_handler._media_cache_key = Mock( @@ -452,8 +462,9 @@ async def _responses(): assert outputs == [{"token_ids": [7]}] cache_handler._media_cache_key.assert_not_called() cache_handler.encoder.encode_mock.assert_awaited_once_with( - [video_url], Modality.VIDEO + [video_bytes], Modality.VIDEO ) + fetch.assert_awaited_once() def test_aux_value_for_item_rejects_mismatched_batched_lists() -> None: @@ -474,10 +485,13 @@ def test_aux_value_for_item_rejects_mismatched_batched_lists() -> None: @pytest.mark.asyncio async def test_video_cache_key_includes_sampling_config( cache_handler: MultimodalEncodeWorkerHandler, + monkeypatch, ) -> None: """Changing video sampling config should force a cache miss for the same URL.""" video_url = "https://example.com/clip.mp4" + fetch = AsyncMock(return_value=b"video-bytes") + monkeypatch.setattr(f"{_HANDLER_MOD}.fetch_bytes", fetch) cache_handler.encoder.encode_mock.return_value = ( torch.tensor([[2, 3, 4]]), torch.arange(24, dtype=torch.float32).reshape(6, 4), @@ -502,6 +516,7 @@ async def test_video_cache_key_includes_sampling_config( assert first_key != second_key assert cache_handler.encoder.encode_mock.await_count == 2 + assert fetch.await_count == 2 assert cache_handler._embedding_cache.get(first_key) is not None assert cache_handler._embedding_cache.get(second_key) is not None @@ -555,8 +570,8 @@ async def test_disabled_nvdec_without_software_decoder_is_actionable( async def test_disabled_nvdec_with_software_decoder_passes_urls( nvdec_handler, monkeypatch ) -> None: - """NVDEC off but a software decoder exists: URLs pass through unchanged - (SGLang fetches and decodes them itself, as before).""" + """NVDEC off but a software decoder exists: non-HTTP URLs pass through + unchanged for SGLang to decode.""" monkeypatch.setenv("DYN_DISABLE_NVDEC", "1") monkeypatch.setattr(f"{_HANDLER_MOD}._software_video_decoder_imports", lambda: True) @@ -564,6 +579,28 @@ async def test_disabled_nvdec_with_software_decoder_passes_urls( assert out == [_INLINE_VIDEO] +@pytest.mark.asyncio +async def test_disabled_nvdec_fetches_remote_video_with_configured_limit( + nvdec_handler, monkeypatch +) -> None: + """Remote URLs must not escape Dynamo's limit through SGLang's downloader.""" + monkeypatch.setenv("DYN_DISABLE_NVDEC", "1") + monkeypatch.setenv(DYN_MM_MAX_FILE_SIZE_MB, "1") + url = "https://example.com/clip.webm" + fetch = AsyncMock(return_value=b"video-bytes") + monkeypatch.setattr(f"{_HANDLER_MOD}.fetch_bytes", fetch) + + out = await nvdec_handler._build_encode_inputs([url], "VIDEO") + + assert out == [b"video-bytes"] + fetch.assert_awaited_once_with( + url, + 30.0, + policy=nvdec_handler._url_policy, + max_bytes=1024 * 1024, + ) + + def test_nvdec_video_enabled_gating(nvdec_handler, monkeypatch) -> None: monkeypatch.setattr(f"{_HANDLER_MOD}.nvdec_available", lambda: True) diff --git a/components/src/dynamo/sglang/tests/test_sglang_multimodal_video.py b/components/src/dynamo/sglang/tests/test_sglang_multimodal_video.py index ee450a0caed3..0bf70476f948 100644 --- a/components/src/dynamo/sglang/tests/test_sglang_multimodal_video.py +++ b/components/src/dynamo/sglang/tests/test_sglang_multimodal_video.py @@ -6,6 +6,7 @@ import json from contextlib import asynccontextmanager from types import SimpleNamespace +from unittest.mock import AsyncMock import numpy as np import pytest @@ -660,7 +661,7 @@ async def test_vp9_without_software_decoder_is_actionable(monkeypatch): async def fake_validate(url, policy): return url - async def fake_fetch(url, timeout, policy=None): + async def fake_fetch(url, timeout, policy=None, max_bytes=None): return b"vp9-bytes" monkeypatch.setattr(ewh, "validate_media_url", fake_validate) @@ -688,7 +689,7 @@ async def test_vp9_with_software_decoder_passes_bytes_through(monkeypatch): async def fake_validate(url, policy): return url - async def fake_fetch(url, timeout, policy=None): + async def fake_fetch(url, timeout, policy=None, max_bytes=None): return b"vp9-bytes" monkeypatch.setattr(ewh, "validate_media_url", fake_validate) @@ -703,6 +704,24 @@ async def fake_fetch(url, timeout, policy=None): assert result == b"vp9-bytes" +@pytest.mark.asyncio +async def test_remote_video_fetch_honors_configured_media_limit(monkeypatch): + import dynamo.sglang.request_handlers.multimodal.encode_worker_handler as ewh + from dynamo.common.http.media_reference import DYN_MM_MAX_FILE_SIZE_MB + + monkeypatch.setenv(DYN_MM_MAX_FILE_SIZE_MB, "1") + fetch = AsyncMock(return_value=b"vp9-bytes") + monkeypatch.setattr(ewh, "fetch_bytes", fetch) + monkeypatch.setattr(ewh, "probe_video_codec", lambda b: "vp9") + monkeypatch.setattr(ewh, "should_use_nvdec", lambda c: False) + monkeypatch.setattr(ewh.importlib, "import_module", _selective_import({"decord"})) + + handler = _bare_handler() + await handler._maybe_nvdec_decoder("https://example.com/clip.webm") + + assert fetch.await_args.kwargs["max_bytes"] == 1024 * 1024 + + @pytest.mark.asyncio async def test_multimodal_decode_rejects_parallel_sampling_before_prefill_handoff(): """Reject multimodal n greater than one before contacting prefill.""" diff --git a/components/src/dynamo/trtllm/multimodal_processor.py b/components/src/dynamo/trtllm/multimodal_processor.py index 46dc23f2d008..9cf7a2f14bfb 100644 --- a/components/src/dynamo/trtllm/multimodal_processor.py +++ b/components/src/dynamo/trtllm/multimodal_processor.py @@ -163,7 +163,8 @@ def __init__( self.tokenizer = tokenizer_factory(model_dir) self.image_loader = ImageLoader( - enable_frontend_decoding=enable_frontend_decoding + enable_frontend_decoding=enable_frontend_decoding, + max_bytes=self.max_file_size_bytes, ) # Reuse the shared default so this preprocessor and the vLLM/SGLang @@ -588,7 +589,10 @@ async def process_openai_request( normalized_url = await validate_media_url(url, self._url_policy) if urlparse(normalized_url).scheme in ("http", "https"): content = await fetch_bytes( - normalized_url, 30.0, policy=self._url_policy + normalized_url, + 30.0, + policy=self._url_policy, + max_bytes=self.max_file_size_bytes, ) # Dual decode path: H.264/H.265 via NVDEC (hardware); other # codecs via the vendor cv2 loader. NVDEC failure falls back. diff --git a/components/src/dynamo/trtllm/tests/test_trtllm_multimodal_processor.py b/components/src/dynamo/trtllm/tests/test_trtllm_multimodal_processor.py index 735a599ac6e8..2fc370448d51 100644 --- a/components/src/dynamo/trtllm/tests/test_trtllm_multimodal_processor.py +++ b/components/src/dynamo/trtllm/tests/test_trtllm_multimodal_processor.py @@ -36,6 +36,23 @@ # sequential GPU stage so TensorRT-LLM initialization is shared. +def test_image_loader_uses_trtllm_configured_limit(monkeypatch) -> None: + image_loader = MagicMock() + monkeypatch.setattr(mmp, "ImageLoader", image_loader) + + MultimodalRequestProcessor( + model_type="multimodal", + model_dir="unused", + max_file_size_mb=200, + tokenizer=MagicMock(), + ) + + image_loader.assert_called_once_with( + enable_frontend_decoding=False, + max_bytes=200 * 1024 * 1024, + ) + + @pytest.mark.asyncio @pytest.mark.parametrize( "error", @@ -101,7 +118,12 @@ async def test_internal_video_uses_dynamo_fetcher_when_allowed(monkeypatch) -> N ep_disaggregated_params=None, ) - fetch.assert_awaited_once_with(url, 30.0, policy=processor._url_policy) + fetch.assert_awaited_once_with( + url, + 30.0, + policy=processor._url_policy, + max_bytes=processor.max_file_size_bytes, + ) assert load_video.await_args.args[0] != url diff --git a/docs/fern/pages/use-cases/multimodal-serving/video-decode-gpu-requirements.md b/docs/fern/pages/use-cases/multimodal-serving/video-decode-gpu-requirements.md index fa41b29c14f4..d12a713da732 100644 --- a/docs/fern/pages/use-cases/multimodal-serving/video-decode-gpu-requirements.md +++ b/docs/fern/pages/use-cases/multimodal-serving/video-decode-gpu-requirements.md @@ -206,6 +206,7 @@ NVDEC as above needs no additional change. Encode performance does not depend on | `DYN_DISABLE_NVDEC` | unset | Set to `1` to skip hardware decode. In a shipped image that leaves video input with no decoder at all, so it is a debugging switch rather than a fallback. Read as a boolean: `1`/`true`/`yes` disable, anything else does not. | | `DYN_NVDEC_GPU_ID` | `0` | GPU ordinal used for decode. | | `DYN_MM_VIDEO_NUM_FRAMES` | `32` | Frames sampled uniformly from each clip. | +| `DYN_MM_MAX_FILE_SIZE_MB` | `64` | Maximum size in MiB for each remote image, audio, or video download. TensorRT-LLM uses its backend-specific `--max-file-size-mb` option (`DYN_TRTLLM_MAX_FILE_SIZE_MB`) instead. | ## Sources