NeMo-Export-Deploy 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/NeMo_Export_Deploy.egg-info/PKG-INFO +3 -3
  2. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/NeMo_Export_Deploy.egg-info/SOURCES.txt +1 -0
  3. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/NeMo_Export_Deploy.egg-info/requires.txt +2 -2
  4. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/PKG-INFO +3 -3
  5. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/inference/inference_base.py +19 -6
  6. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/inference/tron_utils.py +2 -2
  7. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/megatronllm_deployable.py +36 -3
  8. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/megatronllm_deployable_ray.py +2 -0
  9. nemo_export_deploy-0.8.0/nemo_deploy/multimodal/image_url_validator.py +173 -0
  10. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/multimodal/megatron_multimodal_deployable.py +9 -0
  11. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/multimodal/query_multimodal.py +4 -3
  12. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/service/fastapi_interface_to_pytriton.py +1 -1
  13. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/service/fastapi_interface_to_pytriton_multimodal.py +1 -1
  14. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/model_adapters/embedding/embedding_adapter.py +1 -1
  15. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/utils/lora_converter.py +1 -1
  16. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/utils/utils.py +2 -2
  17. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/vllm_exporter.py +0 -4
  18. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export_deploy_common/import_utils.py +1 -1
  19. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export_deploy_common/package_info.py +1 -1
  20. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/pyproject.toml +42 -14
  21. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/LICENSE +0 -0
  22. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/NeMo_Export_Deploy.egg-info/dependency_links.txt +0 -0
  23. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/NeMo_Export_Deploy.egg-info/top_level.txt +0 -0
  24. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/README.md +0 -0
  25. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/__init__.py +0 -0
  26. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/deploy_base.py +0 -0
  27. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/deploy_pytriton.py +0 -0
  28. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/deploy_ray.py +0 -0
  29. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/__init__.py +0 -0
  30. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/hf_deployable.py +0 -0
  31. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/hf_deployable_ray.py +0 -0
  32. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/inference/__init__.py +0 -0
  33. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/inference/nemo_io.py +0 -0
  34. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/inference/nemo_utils.py +0 -0
  35. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/llm/query_llm.py +0 -0
  36. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/multimodal/__init__.py +0 -0
  37. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/multimodal/megatron_multimodal_deployable_ray.py +0 -0
  38. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/package_info.py +0 -0
  39. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/ray_utils.py +0 -0
  40. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/service/__init__.py +0 -0
  41. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/triton_deployable.py +0 -0
  42. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_deploy/utils.py +0 -0
  43. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/__init__.py +0 -0
  44. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/model_adapters/__init__.py +0 -0
  45. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/model_adapters/embedding/__init__.py +0 -0
  46. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/model_adapters/reranker/__init__.py +0 -0
  47. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/model_adapters/reranker/reranker_adapter.py +0 -0
  48. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/onnx_llm_exporter.py +0 -0
  49. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/package_info.py +0 -0
  50. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/sentencepiece_tokenizer.py +0 -0
  51. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/tarutils.py +0 -0
  52. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/tiktoken_tokenizer.py +0 -0
  53. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/utils/__init__.py +0 -0
  54. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/utils/_mock_import.py +0 -0
  55. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/utils/constants.py +0 -0
  56. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export/utils/model_loader.py +0 -0
  57. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/nemo_export_deploy_common/__init__.py +0 -0
  58. {nemo_export_deploy-0.7.0 → nemo_export_deploy-0.8.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NeMo-Export-Deploy
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: NeMo Export and Deploy - a library to export and deploy LLMs and MMs
5
5
  License: Apache 2.0
6
6
  Requires-Python: <3.13,>=3.12
@@ -31,7 +31,7 @@ Requires-Dist: torchmetrics>=0.11.0
31
31
  Requires-Dist: wandb==0.27.2
32
32
  Requires-Dist: webdataset>=0.2.86
33
33
  Requires-Dist: nvidia-pytriton; platform_system != "Darwin"
34
- Requires-Dist: flashinfer-python==0.6.8.post1; platform_system != "Darwin"
34
+ Requires-Dist: flashinfer-python; platform_system != "Darwin"
35
35
  Requires-Dist: Pillow; platform_system != "Darwin" and platform_machine != "aarch64"
36
36
  Requires-Dist: decord; platform_system != "Darwin" and platform_machine != "aarch64"
37
37
  Requires-Dist: pyparsing>2.0.2
@@ -42,7 +42,7 @@ Requires-Dist: ijson
42
42
  Requires-Dist: pyarrow<21.0.0,>=20.0.0
43
43
  Provides-Extra: inframework
44
44
  Provides-Extra: vllm
45
- Requires-Dist: vllm~=0.24.0; extra == "vllm"
45
+ Requires-Dist: vllm~=0.29.0; extra == "vllm"
46
46
  Requires-Dist: pandas; extra == "vllm"
47
47
  Requires-Dist: timm; extra == "vllm"
48
48
  Provides-Extra: trt-onnx
@@ -26,6 +26,7 @@ nemo_deploy/llm/inference/nemo_io.py
26
26
  nemo_deploy/llm/inference/nemo_utils.py
27
27
  nemo_deploy/llm/inference/tron_utils.py
28
28
  nemo_deploy/multimodal/__init__.py
29
+ nemo_deploy/multimodal/image_url_validator.py
29
30
  nemo_deploy/multimodal/megatron_multimodal_deployable.py
30
31
  nemo_deploy/multimodal/megatron_multimodal_deployable_ray.py
31
32
  nemo_deploy/multimodal/query_multimodal.py
@@ -28,7 +28,7 @@ pyarrow<21.0.0,>=20.0.0
28
28
 
29
29
  [:platform_system != "Darwin"]
30
30
  nvidia-pytriton
31
- flashinfer-python==0.6.8.post1
31
+ flashinfer-python
32
32
 
33
33
  [:platform_system != "Darwin" and platform_machine != "aarch64"]
34
34
  Pillow
@@ -48,6 +48,6 @@ onnxscript>=0.6.0
48
48
  transformers==4.51.3
49
49
 
50
50
  [vllm]
51
- vllm~=0.24.0
51
+ vllm~=0.29.0
52
52
  pandas
53
53
  timm
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: NeMo-Export-Deploy
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: NeMo Export and Deploy - a library to export and deploy LLMs and MMs
5
5
  License: Apache 2.0
6
6
  Requires-Python: <3.13,>=3.12
@@ -31,7 +31,7 @@ Requires-Dist: torchmetrics>=0.11.0
31
31
  Requires-Dist: wandb==0.27.2
32
32
  Requires-Dist: webdataset>=0.2.86
33
33
  Requires-Dist: nvidia-pytriton; platform_system != "Darwin"
34
- Requires-Dist: flashinfer-python==0.6.8.post1; platform_system != "Darwin"
34
+ Requires-Dist: flashinfer-python; platform_system != "Darwin"
35
35
  Requires-Dist: Pillow; platform_system != "Darwin" and platform_machine != "aarch64"
36
36
  Requires-Dist: decord; platform_system != "Darwin" and platform_machine != "aarch64"
37
37
  Requires-Dist: pyparsing>2.0.2
@@ -42,7 +42,7 @@ Requires-Dist: ijson
42
42
  Requires-Dist: pyarrow<21.0.0,>=20.0.0
43
43
  Provides-Extra: inframework
44
44
  Provides-Extra: vllm
45
- Requires-Dist: vllm~=0.24.0; extra == "vllm"
45
+ Requires-Dist: vllm~=0.29.0; extra == "vllm"
46
46
  Requires-Dist: pandas; extra == "vllm"
47
47
  Requires-Dist: timm; extra == "vllm"
48
48
  Provides-Extra: trt-onnx
@@ -16,6 +16,7 @@
16
16
  import atexit
17
17
  import logging
18
18
  import os
19
+ import socket
19
20
  from pathlib import Path
20
21
  from typing import List, Optional, Tuple, Union
21
22
 
@@ -414,6 +415,22 @@ class MCoreEngineWithCleanup:
414
415
  return getattr(self.mcore_engine, name)
415
416
 
416
417
 
418
+ def _default_coordinator_host() -> str:
419
+ """Return a bindable address for the data-parallel inference coordinator.
420
+
421
+ MCore defaults the coordinator host to ``socket.gethostname()``, which is a
422
+ *name*, not an address. Inside Docker it resolves to the container ID and ZMQ
423
+ cannot bind it (``ZMQError: No such device``). Resolving that name to its IP
424
+ yields an address that is both bindable and reachable from other containers,
425
+ which is what the Ray path already gets by exporting the node IP as
426
+ ``MASTER_ADDR``. Fall back to loopback only if resolution fails.
427
+ """
428
+ try:
429
+ return socket.gethostbyname(socket.gethostname())
430
+ except OSError:
431
+ return "127.0.0.1"
432
+
433
+
417
434
  def create_mcore_engine(
418
435
  path: Path,
419
436
  num_devices: Optional[int] = None,
@@ -519,14 +536,10 @@ def create_mcore_engine(
519
536
  buffer_size_gb=int(buffer_size_gb),
520
537
  max_requests=max_batch_size,
521
538
  block_size_tokens=block_size_tokens,
522
- # Coordinator mode constructs one DynamicInferenceEngine per rank independently, so a
523
- # per-request runtime toggle (as _infer_fn used to do) only ever reaches the primary
524
- # rank's engine. Prompt log probs require this to be False on every rank, so it's set
525
- # here unconditionally at construction time instead.
526
- materialize_only_last_token_logits=False,
539
+ materialize_only_last_token_logits=True,
527
540
  )
528
541
 
529
- coordinator_host = os.environ.get("MASTER_ADDR")
542
+ coordinator_host = os.environ.get("MASTER_ADDR") or _default_coordinator_host()
530
543
 
531
544
  llm = MegatronLLM(
532
545
  model=model,
@@ -261,7 +261,7 @@ def _set_random_seed(
261
261
  te_rng_tracker: bool = False,
262
262
  inference_rng_tracker: bool = False,
263
263
  ) -> None:
264
- """Set random seed for reproducability.
264
+ """Set random seed for reproducibility.
265
265
 
266
266
  Args:
267
267
  seed_ (int): Base random seed to use
@@ -332,7 +332,7 @@ def _initialize_tp_communicators(model_config: Union[GPTConfig, T5Config], micro
332
332
  bootstrap_backend=bootstrap_backend,
333
333
  )
334
334
  except TypeError:
335
- # Fallback for older TE versions
335
+ # Fallback for older Transformer Engine versions
336
336
  if bootstrap_backend != "mpi":
337
337
  LOGGER.info("Warning: Transformer Engine may only support MPI bootstrap backend")
338
338
 
@@ -69,7 +69,7 @@ class MegatronLLMDeployable(ITritonDeployable):
69
69
  context_parallel_size (int): context parallelism.
70
70
  expert_model_parallel_size (int): expert parallelism.
71
71
  params_dtype (torch.dtype): max input length.
72
- inference_batch_times_seqlen_threshold (int): squence threshold.
72
+ inference_batch_times_seqlen_threshold (int): sequence threshold.
73
73
  inference_max_seq_length (int): max_seq_length for inference. Required by MCoreEngine (>=0.12). Defaults to
74
74
  4096.
75
75
  max_batch_size (int): max batch size for inference. Defaults to 32.
@@ -205,6 +205,12 @@ class MegatronLLMDeployable(ITritonDeployable):
205
205
  stop_words=stop_words,
206
206
  )
207
207
 
208
+ if log_probs:
209
+ dynamic_engine = getattr(self.mcore_engine, "engine", None)
210
+ if dynamic_engine is not None:
211
+ dynamic_engine.materialize_only_last_token_logits = False
212
+ dynamic_engine.context.config.materialize_only_last_token_logits = False
213
+
208
214
  self.generate(prompts, inference_params)
209
215
  else:
210
216
  return
@@ -394,6 +400,22 @@ class MegatronLLMDeployable(ITritonDeployable):
394
400
  if apply_chat_template:
395
401
  prompts = [self.apply_chat_template(prompt) for prompt in prompts]
396
402
 
403
+ if torch.distributed.is_initialized():
404
+ if torch.distributed.get_world_size() > 1:
405
+ torch.distributed.broadcast(torch.tensor([0], dtype=torch.long, device="cuda"), src=0)
406
+ broadcast_list(prompts, src=0)
407
+ broadcast_list(
408
+ data=[
409
+ temperature,
410
+ top_k,
411
+ top_p,
412
+ num_tokens_to_generate,
413
+ log_probs,
414
+ stop_words,
415
+ ],
416
+ src=0,
417
+ )
418
+
397
419
  # cast top_k,top_p to native int, float since typecheck assert statements added in MCore0.13 error otherwise
398
420
  # skip_prompt_log_probs=False (default) includes prompt tokens in top-N logprobs when top_logprobs>0.
399
421
  inference_params = SamplingParams(
@@ -407,6 +429,17 @@ class MegatronLLMDeployable(ITritonDeployable):
407
429
  stop_words=stop_words,
408
430
  )
409
431
 
432
+ # Mcore's dynamic inference engine defaults materialize_only_last_token_logits=True for
433
+ # performance, but prompt log probs require all token logits to be materialized
434
+ # (prompt log probs are required for logprob eval benchmarks).
435
+ # Toggle it on both the engine and the context config (controls the
436
+ # model forward pass and log prob calculations).
437
+ dynamic_engine = getattr(self.mcore_engine, "engine", None)
438
+ needs_all_logits = log_probs or bool(top_logprobs)
439
+ if dynamic_engine is not None and needs_all_logits:
440
+ dynamic_engine.materialize_only_last_token_logits = False
441
+ dynamic_engine.context.config.materialize_only_last_token_logits = False
442
+
410
443
  results = self.generate(prompts, inference_params)
411
444
  # Handle DynamicInferenceRequestRecord objects by merging them into a single request
412
445
  results = [
@@ -422,7 +455,7 @@ class MegatronLLMDeployable(ITritonDeployable):
422
455
  if log_probs:
423
456
  output_log_probs = []
424
457
  for r in results:
425
- # Convert to torch tensor and then move to cpu as generated_log_probs is a list and cant be moved
458
+ # Convert to torch tensor and then move to cpu as generated_log_probs is a list and cannot be moved
426
459
  # to cpu otherwise
427
460
  if echo:
428
461
  lp = torch.tensor(r.prompt_log_probs + r.generated_log_probs).cpu().detach().numpy()
@@ -448,7 +481,7 @@ class MegatronLLMDeployable(ITritonDeployable):
448
481
  if top_logprobs:
449
482
  output_top_n_log_probs = []
450
483
  for r in results:
451
- # Convert to torch tensor and then move to cpu as generated_log_probs is a list and cant be moved
484
+ # Convert to torch tensor and then move to cpu as generated_log_probs is a list and cannot be moved
452
485
  # to cpu otherwise.
453
486
  # top_logprobs for input tokens is supported with MCore 0.13 and above.
454
487
  if echo:
@@ -104,6 +104,8 @@ class ModelWorker:
104
104
  legacy_model_format=legacy_model_format,
105
105
  **model_config_kwargs,
106
106
  )
107
+ if rank != 0:
108
+ self.model.generate_other_ranks()
107
109
  except Exception as e:
108
110
  LOGGER.error(f"Replica {replica_id} - Failed to initialize model for rank {rank}: {str(e)}")
109
111
  raise
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ import base64
18
+ import ipaddress
19
+ import socket
20
+ from io import BytesIO
21
+ from urllib.parse import urljoin, urlparse
22
+
23
+ import urllib3
24
+
25
+ # Ranges that must never be reachable via a request-controlled image URL.
26
+ # 169.254.0.0/16 is the cloud IMDS range (AWS/GCP/Azure 169.254.169.254) —
27
+ # the primary SSRF target in cloud deployments.
28
+ _BLOCKED_NETWORKS = [
29
+ ipaddress.ip_network("127.0.0.0/8"), # loopback — server's own local services
30
+ ipaddress.ip_network("10.0.0.0/8"), # RFC 1918 private
31
+ ipaddress.ip_network("172.16.0.0/12"), # RFC 1918 private
32
+ ipaddress.ip_network("192.168.0.0/16"), # RFC 1918 private
33
+ ipaddress.ip_network("169.254.0.0/16"), # link-local / cloud IMDS
34
+ ipaddress.ip_network("::1/128"), # IPv6 loopback
35
+ ipaddress.ip_network("fc00::/7"), # IPv6 unique-local
36
+ ipaddress.ip_network("fe80::/10"), # IPv6 link-local (IPv6 cloud IMDS equivalent)
37
+ ]
38
+
39
+
40
+ def _is_blocked_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
41
+ """Return True if ip must not be reachable via an image URL fetch."""
42
+ if any(ip in net for net in _BLOCKED_NETWORKS):
43
+ return True
44
+ return not ip.is_global or ip.is_multicast or ip.is_reserved or ip.is_unspecified
45
+
46
+
47
+ def _resolve_and_validate(hostname: str) -> list[tuple[int, str]]:
48
+ """Resolve hostname to every A/AAAA address and reject if any is blocked.
49
+
50
+ Returns a list of (address_family, ip_string) tuples for all resolved
51
+ addresses. Validating every returned address (rather than just the
52
+ first) guards against DNS answers that mix a public address with a
53
+ private/link-local one (DNS rebinding / multi-answer SSRF).
54
+ """
55
+ try:
56
+ infos = socket.getaddrinfo(hostname, None)
57
+ except socket.gaierror as exc:
58
+ raise ValueError(f"Cannot resolve image URL hostname '{hostname}': {exc}") from exc
59
+ resolved: list[tuple[int, str]] = []
60
+ seen: set[str] = set()
61
+ for family, _, _, _, sockaddr in infos:
62
+ ip_str = sockaddr[0]
63
+ if ip_str in seen:
64
+ continue
65
+ seen.add(ip_str)
66
+ ip = ipaddress.ip_address(ip_str)
67
+ if _is_blocked_ip(ip):
68
+ raise ValueError(
69
+ f"Image URL hostname '{hostname}' resolves to a blocked address ({ip}). "
70
+ "Private, loopback, link-local, and other non-global addresses are not allowed."
71
+ )
72
+ resolved.append((family, ip_str))
73
+ if not resolved:
74
+ raise ValueError(f"Cannot resolve image URL hostname '{hostname}': no addresses returned.")
75
+ return resolved
76
+
77
+
78
+ def validate_image_url(url: str) -> None:
79
+ """Raise ValueError if url is not a safe http/https URL.
80
+
81
+ Rejects file://, non-http(s) schemes, and URLs that resolve to
82
+ private/link-local/loopback ranges (SSRF guard). This is a point-in-time
83
+ check; use fetch_image_bytes_safely()/fetch_image_data_uri_safely() to
84
+ actually retrieve the image, since those pin validation to the
85
+ connection itself and guard redirects.
86
+ """
87
+ parsed = urlparse(url)
88
+ if parsed.scheme not in ("http", "https"):
89
+ raise ValueError(f"Unsupported image URL scheme '{parsed.scheme}'. Only http and https are allowed.")
90
+ hostname = parsed.hostname
91
+ if not hostname:
92
+ raise ValueError("Image URL has no hostname.")
93
+ _resolve_and_validate(hostname)
94
+
95
+
96
+ def _pinned_request(url: str, hostname: str, ip: str, family: int, timeout: float) -> urllib3.HTTPResponse:
97
+ """Issue a GET request connected directly to ip, without following redirects.
98
+
99
+ Connecting to the pre-validated IP directly (instead of letting the HTTP
100
+ client re-resolve hostname) closes the TOCTOU window between validation
101
+ and connection that DNS rebinding exploits. The Host header and TLS SNI
102
+ still use the original hostname so virtual hosting and certificate
103
+ validation behave normally.
104
+ """
105
+ parsed = urlparse(url)
106
+ port = parsed.port or (443 if parsed.scheme == "https" else 80)
107
+ path = parsed.path or "/"
108
+ if parsed.query:
109
+ path = f"{path}?{parsed.query}"
110
+ pool_host = f"[{ip}]" if family == socket.AF_INET6 else ip
111
+ headers = {"Host": hostname}
112
+ if parsed.scheme == "https":
113
+ pool = urllib3.HTTPSConnectionPool(
114
+ pool_host,
115
+ port=port,
116
+ timeout=timeout,
117
+ assert_hostname=hostname,
118
+ server_hostname=hostname,
119
+ cert_reqs="CERT_REQUIRED",
120
+ )
121
+ else:
122
+ pool = urllib3.HTTPConnectionPool(pool_host, port=port, timeout=timeout)
123
+ try:
124
+ return pool.request("GET", path, headers=headers, redirect=False, preload_content=True)
125
+ finally:
126
+ pool.close()
127
+
128
+
129
+ def fetch_image_bytes_safely(url: str, timeout: float = 5, max_redirects: int = 5) -> bytes:
130
+ """Safely fetch image bytes from url, guarding against SSRF.
131
+
132
+ Unlike validate_image_url() followed by a separate request, this
133
+ resolves and validates every hostname (including redirect targets)
134
+ immediately before connecting, connects directly to the validated IP
135
+ address, and never auto-follows redirects — each hop is re-validated
136
+ from scratch.
137
+ """
138
+ current_url = url
139
+ for _ in range(max_redirects + 1):
140
+ parsed = urlparse(current_url)
141
+ if parsed.scheme not in ("http", "https"):
142
+ raise ValueError(f"Unsupported image URL scheme '{parsed.scheme}'. Only http and https are allowed.")
143
+ hostname = parsed.hostname
144
+ if not hostname:
145
+ raise ValueError("Image URL has no hostname.")
146
+ resolved = _resolve_and_validate(hostname)
147
+ family, ip = resolved[0]
148
+ response = _pinned_request(current_url, hostname, ip, family, timeout)
149
+ if response.status in (301, 302, 303, 307, 308):
150
+ location = response.headers.get("Location")
151
+ if not location:
152
+ raise ValueError(f"Redirect response from '{current_url}' is missing a Location header.")
153
+ current_url = urljoin(current_url, location)
154
+ continue
155
+ if response.status >= 400:
156
+ raise ValueError(f"Failed to fetch image URL '{current_url}': HTTP {response.status}")
157
+ return response.data
158
+ raise ValueError(f"Exceeded max redirects ({max_redirects}) while fetching image URL '{url}'.")
159
+
160
+
161
+ def fetch_image_data_uri_safely(url: str, timeout: float = 5, max_redirects: int = 5) -> str:
162
+ """Safely fetch an image and return it as a base64 data URI.
163
+
164
+ Useful for handing an image to code (e.g. third-party libraries) that
165
+ would otherwise perform its own, unguarded network fetch given a raw
166
+ URL — the network I/O happens here, under the SSRF guard, instead.
167
+ """
168
+ from PIL import Image
169
+
170
+ data = fetch_image_bytes_safely(url, timeout=timeout, max_redirects=max_redirects)
171
+ image_format = (Image.open(BytesIO(data)).format or "JPEG").lower()
172
+ encoded = base64.b64encode(data).decode("ascii")
173
+ return f"data:image/{image_format};base64,{encoded}"
@@ -173,6 +173,15 @@ class MegatronMultimodalDeployable(ITritonDeployable):
173
173
  if isinstance(self.inference_wrapped_model, QwenVLInferenceWrapper):
174
174
  from qwen_vl_utils import process_vision_info
175
175
 
176
+ from nemo_deploy.multimodal.image_url_validator import fetch_image_data_uri_safely
177
+
178
+ # data: URIs are inline base64 and never trigger a network request.
179
+ # All other values are treated as URLs: fetch them ourselves under the SSRF
180
+ # guard and hand qwen_vl_utils a data URI, since it would otherwise perform
181
+ # its own unguarded network fetch for a raw URL.
182
+ if not image_source.startswith("data:"):
183
+ image_source = fetch_image_data_uri_safely(image_source)
184
+
176
185
  messages = [
177
186
  {
178
187
  "role": "user",
@@ -17,7 +17,6 @@ from io import BytesIO
17
17
  from typing import List, Optional
18
18
 
19
19
  import numpy as np
20
- import requests
21
20
 
22
21
  from nemo_deploy.utils import str_list2numpy
23
22
  from nemo_export_deploy_common.import_utils import (
@@ -100,8 +99,10 @@ class NemoQueryMultimodal:
100
99
  raise UnavailableError(MISSING_PIL_MSG)
101
100
 
102
101
  if input_media.startswith("http") or input_media.startswith("https"):
103
- response = requests.get(input_media, timeout=5)
104
- media = Image.open(BytesIO(response.content)).convert("RGB")
102
+ from nemo_deploy.multimodal.image_url_validator import fetch_image_bytes_safely
103
+
104
+ image_bytes = fetch_image_bytes_safely(input_media, timeout=5)
105
+ media = Image.open(BytesIO(image_bytes)).convert("RGB")
105
106
  else:
106
107
  media = Image.open(input_media).convert("RGB")
107
108
  return np.expand_dims(np.array(media), axis=0)
@@ -276,7 +276,7 @@ async def chat_completions_v1(request: ChatCompletionRequest):
276
276
  prompts = request.messages
277
277
  if not isinstance(request.messages, list):
278
278
  prompts = [request.messages]
279
- # Serialize the dictionary to a JSON string represnetation to be able to convert to numpy array
279
+ # Serialize the dictionary to a JSON string representation to be able to convert to numpy array
280
280
  # (str_list2numpy) and back to list (str_ndarray2list) as required by PyTriton. Using the dictionaries directly
281
281
  # with these methods is not possible as they expect string type.
282
282
  json_prompts = [dict_to_str(prompts)]
@@ -298,7 +298,7 @@ async def chat_completions_v1(request: MultimodalChatCompletionRequest):
298
298
  # Remove image_url field
299
299
  content.pop("image_url", None)
300
300
 
301
- # Serialize the dictionary to a JSON string represnetation to be able to convert to numpy array
301
+ # Serialize the dictionary to a JSON string representation to be able to convert to numpy array
302
302
  # (str_list2numpy) and back to list (str_ndarray2list) as required by PyTriton. Using the dictionaries directly
303
303
  # with these methods is not possible as they expect string type.
304
304
  json_prompts = [dict_to_str(prompts)]
@@ -255,7 +255,7 @@ def get_llama_bidirectional_hf_model(
255
255
  # configure pooling
256
256
  pooling_module = Pooling(pooling_mode=pooling_mode)
257
257
 
258
- # NV-Embed-v1 model has seperate embedding model and a built-in pooling module
258
+ # NV-Embed-v1 model has separate embedding model and a built-in pooling module
259
259
  if (
260
260
  model.__class__.__name__ == "NVEmbedModel"
261
261
  and hasattr(model, "latent_attention_model")
@@ -201,7 +201,7 @@ def convert_lora_nemo_to_canonical(lora_nemo, save_path, hf_format=False, donor_
201
201
  new_key = replace_number_add_offset(key, layer_offset)
202
202
  lora_state_dict[tp][new_key] = value
203
203
 
204
- # TODO: currently suport tp=1
204
+ # TODO: currently support tp=1
205
205
  lora_state_dict = lora_state_dict[0]
206
206
  if lora_config["peft"]["lora_tuning"].get("variant", "nemo") == "nemo":
207
207
  lora_config["peft"]["lora_tuning"]["variant"] = "canonical"
@@ -120,10 +120,10 @@ def get_example_inputs(
120
120
 
121
121
 
122
122
  def validate_fp8_network(network) -> None:
123
- """Checks the network to ensure it's compatible with fp8 precison.
123
+ """Checks the network to ensure it's compatible with fp8 precision.
124
124
 
125
125
  Raises:
126
- ValueError if netowrk doesn't container Q/DQ FP8 layers
126
+ ValueError if network doesn't container Q/DQ FP8 layers
127
127
  """
128
128
  import tensorrt as trt
129
129
 
@@ -112,7 +112,6 @@ class vLLMExporter(ITritonDeployable):
112
112
  quantization: str = None,
113
113
  seed: int = 0,
114
114
  gpu_memory_utilization: float = 0.9,
115
- swap_space: float = 4,
116
115
  cpu_offload_gb: float = 0,
117
116
  enforce_eager: bool = False,
118
117
  task: Literal["auto", "generate", "embedding"] = "auto",
@@ -132,7 +131,6 @@ class vLLMExporter(ITritonDeployable):
132
131
  quantization (str, optional): Quantization type. Defaults to None.
133
132
  seed (int, optional): Random seed. Defaults to 0.
134
133
  gpu_memory_utilization (float, optional): Fraction of GPU memory to use. Defaults to 0.9.
135
- swap_space (float, optional): Amount of swap space (in GB) to use. Defaults to 4.
136
134
  cpu_offload_gb (float, optional): Amount of CPU offload memory (in GB). Defaults to 0.
137
135
  enforce_eager (bool, optional): Whether to enforce eager execution. Defaults to False.
138
136
  task (Literal["auto", "generate", "embedding"], optional): Task type for vLLM. Defaults to "auto".
@@ -203,7 +201,6 @@ class vLLMExporter(ITritonDeployable):
203
201
  quantization=quantization,
204
202
  seed=seed,
205
203
  gpu_memory_utilization=gpu_memory_utilization,
206
- swap_space=swap_space,
207
204
  cpu_offload_gb=cpu_offload_gb,
208
205
  enforce_eager=enforce_eager,
209
206
  runner=task,
@@ -219,7 +216,6 @@ class vLLMExporter(ITritonDeployable):
219
216
  quantization=quantization,
220
217
  seed=seed,
221
218
  gpu_memory_utilization=gpu_memory_utilization,
222
- swap_space=swap_space,
223
219
  cpu_offload_gb=cpu_offload_gb,
224
220
  enforce_eager=enforce_eager,
225
221
  runner=task,
@@ -394,7 +394,7 @@ def gpu_only_import(module, *, alt=None) -> Tuple[object, bool]:
394
394
 
395
395
  return safe_import(
396
396
  module,
397
- msg=f"{module} is not enabled in non GPU-enabled installations or environemnts. {GPU_INSTALL_STRING}",
397
+ msg=f"{module} is not enabled in non GPU-enabled installations or environments. {GPU_INSTALL_STRING}",
398
398
  alt=alt,
399
399
  )
400
400
 
@@ -14,7 +14,7 @@
14
14
 
15
15
 
16
16
  MAJOR = 0
17
- MINOR = 7
17
+ MINOR = 8
18
18
  PATCH = 0
19
19
  PRE_RELEASE = ""
20
20
 
@@ -67,7 +67,7 @@ dependencies = [
67
67
  "wandb==0.27.2",
68
68
  "webdataset>=0.2.86",
69
69
  "nvidia-pytriton ; platform_system != 'Darwin' ",
70
- "flashinfer-python==0.6.8.post1 ; platform_system != 'Darwin'",
70
+ "flashinfer-python; platform_system != 'Darwin'",
71
71
  "Pillow ; platform_system != 'Darwin' and platform_machine != 'aarch64'",
72
72
  "decord ; platform_system != 'Darwin' and platform_machine != 'aarch64'",
73
73
  "pyparsing>2.0.2",
@@ -80,7 +80,7 @@ dependencies = [
80
80
 
81
81
  [project.optional-dependencies]
82
82
  inframework = []
83
- vllm = ["vllm~=0.24.0", "pandas", "timm"]
83
+ vllm = ["vllm~=0.29.0", "pandas", "timm"]
84
84
  trt-onnx = [
85
85
  "tensorrt==10.16.1.11",
86
86
  "onnx==1.21.0",
@@ -106,11 +106,15 @@ nemo-run = ["nemo-run"]
106
106
  [tool.uv.sources]
107
107
  xformers = [{ index = "pytorch-cu130" }]
108
108
  torch = [{ index = "pytorch-cu130" }]
109
+ # flashinfer-cubin isn't published to PyPI since 0.6.14; both packages are pulled from
110
+ # flashinfer's own wheel index, same as vLLM's requirements/cuda.txt.
111
+ flashinfer-python = [{ index = "flashinfer" }]
112
+ flashinfer-cubin = [{ index = "flashinfer" }]
109
113
  vllm = [
110
114
  { index = "pytorch-cu130", marker = "python_version < '3.9' and platform_machine == 'x86_64'" },
111
115
  { index = "pypi", marker = "platform_machine == 'aarch64'" },
112
116
  ]
113
- megatron-bridge = { git = "https://github.com/NVIDIA-NeMo/Megatron-Bridge.git", rev = "97e8dba4060ae74a110b9da6255eb0a66b016b2d" }
117
+ megatron-bridge = { git = "https://github.com/NVIDIA-NeMo/Megatron-Bridge.git", rev = "02fcce5a103ca0c88a945319075348729e0b666e" }
114
118
  # Use the released nvidia-resiliency-ext (>=0.6.0) rather than a git dev build:
115
119
  # megatron-core's runtime is_nvrx_min_version() asserts the installed package
116
120
  # version is >=0.6.0, and a git build reports 0.6.0.devN (< 0.6.0), which fails
@@ -150,7 +154,6 @@ override-dependencies = [
150
154
  "transformer-engine[pytorch]>=2.16.0a0,<2.19.0",
151
155
  "open-clip-torch>=3.2.0",
152
156
  "megatron-energon[av-decode]>=6.0,<7.dev0",
153
- "flashinfer-python==0.6.8.post1",
154
157
  "datasets>=3.3.0",
155
158
  "flash-linear-attention>=0.3.0,<0.4.dev0",
156
159
  "patchelf; sys_platform=='never'",
@@ -162,10 +165,16 @@ override-dependencies = [
162
165
  "transformers==5.8.1",
163
166
  "protobuf~=6.33.5",
164
167
  "opencv-python-headless; sys_platform == 'never'",
165
- "cryptography>=43.0.0,<47",
168
+ "cryptography>=50.0.0,<51",
166
169
  "onnx<1.20.0",
167
- "flashinfer-python==0.6.8.post1",
168
- "flashinfer-cubin==0.6.8.post1",
170
+ # flashinfer-python and flashinfer-cubin must be kept in lockstep (flashinfer-python does a
171
+ # runtime version-match check); flashinfer-cubin isn't published to PyPI since 0.6.14, so both
172
+ # are sourced from flashinfer's own wheel index below. Pin matches vLLM's own requirements/cuda.txt.
173
+ "flashinfer-python==0.6.18.post1",
174
+ "flashinfer-cubin==0.6.18.post1",
175
+ "fast-hadamard-transform @ git+https://github.com/Dao-AILab/fast-hadamard-transform.git@v1.1.0", # pin 1.1.0; PyPI sdist is incomplete, install from git tag (see DSv4/GLM-5 READMEs)
176
+ "nvidia-cudnn-frontend==1.26.0",
177
+ "nvidia-cutlass-dsl[cu13]; sys_platform == 'never'", # Rely on system site packages install
169
178
  ]
170
179
  prerelease = "allow"
171
180
 
@@ -173,6 +182,16 @@ prerelease = "allow"
173
182
  name = "nvidia-resiliency-ext"
174
183
  version = "0.6.0"
175
184
 
185
+ [[tool.uv.dependency-metadata]]
186
+ name = "fast-hadamard-transform"
187
+ version = "1.1.0"
188
+ requires-dist = ["torch", "packaging", "ninja"]
189
+
190
+ # torch-memory-saver's build backend reads TMS_CUDA_MAJOR at build time. Keep this in
191
+ # sync with docker/Dockerfile's BASE_IMAGE CUDA major version (currently cuda13.x).
192
+ [tool.uv.extra-build-variables]
193
+ torch-memory-saver = { TMS_CUDA_MAJOR = "13" }
194
+
176
195
  [[tool.uv.index]]
177
196
  name = "pypi"
178
197
  url = "https://pypi.org/simple"
@@ -183,6 +202,19 @@ name = "pytorch-cu130"
183
202
  url = "https://download.pytorch.org/whl/cu130"
184
203
  explicit = true
185
204
 
205
+ [[tool.uv.index]]
206
+ name = "flashinfer"
207
+ url = "https://flashinfer.ai/whl/"
208
+ explicit = true
209
+
210
+ # `[tool.uv.sources]`/`[[tool.uv.index]]` above are only honored by uv's project interface
211
+ # (uv sync/lock/add). The release CI builds the wheel via the pip-compatible interface
212
+ # (`uv pip install -e .`), which does NOT read that config and only reads `[tool.uv.pip]`.
213
+ # flashinfer-cubin/flashinfer-python aren't published on PyPI since 0.6.14, so without this
214
+ # extra index `uv pip install -e .` can't resolve megatron-bridge's flashinfer-cubin pin.
215
+ [tool.uv.pip]
216
+ extra-index-url = ["https://flashinfer.ai/whl/"]
217
+
186
218
  [tool.pytest.ini_options]
187
219
  addopts = "--durations=15 -s -rA -x"
188
220
  testpaths = ["tests"]
@@ -191,18 +223,14 @@ python_files = "test_*.py"
191
223
 
192
224
  [tool.coverage.run]
193
225
  concurrency = ["thread", "multiprocessing"]
226
+ relative_files = true
194
227
  omit = [
195
- "/tmp/*",
196
- "/workspace/tests/*",
197
- "/workspace/nemo_export_deploy_common/*",
198
- "/workspace/*.py",
228
+ "*/tests/*",
229
+ "*/nemo_export_deploy_common/*",
199
230
  "*_ray.py",
200
231
  "*package_info.py",
201
232
  ]
202
233
 
203
- [tool.coverage.paths]
204
- source = [".", "/workspace", "/home/runner/work/Export-Deploy/Export-Deploy"]
205
-
206
234
  [tool.ruff]
207
235
  line-length = 120
208
236