L0SG commited on
Commit
b1a5636
·
verified ·
1 Parent(s): fcf32f0

Add native Transformers inference backend

Browse files

Add a Transformers 5.14 native Nemotron-H audio adapter and selectable native/legacy Hugging Face inference backends. Document the validated dependency versions. No checkpoint weights or vLLM files are changed.

README.md CHANGED
@@ -73,7 +73,13 @@ backbone with marginal or no regression. Audex-30B-A3B operates in both **thinki
73
  We use **vLLM 0.20.0 container image**: [vllm/vllm-openai:v0.20.0-cu129](https://hub.docker.com/layers/vllm/vllm-openai/v0.20.0-cu129/images/sha256-f4ace3494896eeda800dee284d1fc42ca7f5626f31ceae8e24d1383d770567c2)
74
 
75
  - **vLLM inference** — text-only reasoning, text-to-speech, text-to-audio, and audio understanding / speech recognition / speech translation: runs on **vLLM 0.20.0**.
76
- - **Hugging Face / transformers inference** — requires transformers >= 4.53.0 (tested with 4.53.3) and also works with transformers >= 5.0. This additionally needs `mamba-ssm` and `causal-conv1d` (build against your CUDA toolchain, e.g. ```pip install --no-build-isolation causal-conv1d==1.6.2.post1 mamba-ssm==2.3.2.post1```).
 
 
 
 
 
 
77
 
78
  **Audio extras:** ```vllm/vllm-openai:v0.20.0``` image does not include audio codecs. This command installs audio-related packages: ```python3 -m pip install "vllm[audio]"```.
79
 
 
73
  We use **vLLM 0.20.0 container image**: [vllm/vllm-openai:v0.20.0-cu129](https://hub.docker.com/layers/vllm/vllm-openai/v0.20.0-cu129/images/sha256-f4ace3494896eeda800dee284d1fc42ca7f5626f31ceae8e24d1383d770567c2)
74
 
75
  - **vLLM inference** — text-only reasoning, text-to-speech, text-to-audio, and audio understanding / speech recognition / speech translation: runs on **vLLM 0.20.0**.
76
+ - **Hugging Face / transformers inference** — requires transformers >= 4.53.0 (tested with 4.53.3) and also works with transformers >=
77
+ 5.0. This additionally needs `mamba-ssm` and `causal-conv1d`. Build against your CUDA toolchain:
78
+
79
+ ```bash
80
+ python3 -m pip install transformers==5.14.0 safetensors==0.8.0
81
+ python3 -m pip install --no-build-isolation causal-conv1d==1.6.2.post1 mamba-ssm==2.3.1
82
+ ```
83
 
84
  **Audio extras:** ```vllm/vllm-openai:v0.20.0``` image does not include audio codecs. This command installs audio-related packages: ```python3 -m pip install "vllm[audio]"```.
85
 
checkpoint_folder_full/modeling_nemotron_h_audio_native.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
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
+ """Audex audio wrapper for the native Transformers Nemotron-H backbone."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Optional
20
+
21
+ import torch
22
+ import torch.nn.functional as F
23
+ from torch import nn
24
+ from transformers.conversion_mapping import get_checkpoint_conversion_mapping, register_checkpoint_conversion_mapping
25
+ from transformers.models.nemotron_h.configuration_nemotron_h import NemotronHConfig
26
+ from transformers.models.nemotron_h.modeling_nemotron_h import NemotronHForCausalLM
27
+
28
+
29
+ class NemotronHAudexConfig(NemotronHConfig):
30
+ """Native Nemotron-H config plus Audex audio metadata."""
31
+
32
+ model_type = "nemotron_h_audex"
33
+
34
+ def __init__(
35
+ self,
36
+ audio_config: Optional[dict[str, Any]] = None,
37
+ audio_model_type: str = "NV-Whisper",
38
+ sound_model_type: Optional[str] = None,
39
+ audio_preprocessor_path: str = "audio_preprocessor",
40
+ sound_token: str = "<so_embedding>",
41
+ sound_start_token: str = "<so_start>",
42
+ sound_end_token: str = "<so_end>",
43
+ sound_token_id: Optional[int] = None,
44
+ sound_start_token_id: Optional[int] = None,
45
+ sound_end_token_id: Optional[int] = None,
46
+ sound_embedding_size: int = 750,
47
+ sound_clip_duration: float = 30.0,
48
+ sound_target_rate: int = 16000,
49
+ audio_encoder_hidden_size: int = 1280,
50
+ audio_projector_intermediate_size: int = 4096,
51
+ audio_projector_activation: str = "relu2",
52
+ audio_projector_norm_eps: float = 1e-5,
53
+ **kwargs,
54
+ ):
55
+ self.audio_config = audio_config or {
56
+ "model_type": "qwen2_audio_encoder",
57
+ "num_mel_bins": 128,
58
+ "encoder_layers": 32,
59
+ "encoder_attention_heads": 20,
60
+ "encoder_ffn_dim": 5120,
61
+ "d_model": audio_encoder_hidden_size,
62
+ "activation_function": "gelu",
63
+ "scale_embedding": False,
64
+ "max_source_positions": 1500,
65
+ }
66
+ self.audio_model_type = audio_model_type
67
+ self.sound_model_type = sound_model_type
68
+ self.audio_preprocessor_path = audio_preprocessor_path
69
+ self.sound_token = sound_token
70
+ self.sound_start_token = sound_start_token
71
+ self.sound_end_token = sound_end_token
72
+ self.sound_token_id = sound_token_id
73
+ self.sound_start_token_id = sound_start_token_id
74
+ self.sound_end_token_id = sound_end_token_id
75
+ self.sound_embedding_size = sound_embedding_size
76
+ self.sound_clip_duration = sound_clip_duration
77
+ self.sound_target_rate = sound_target_rate
78
+ self.audio_encoder_hidden_size = audio_encoder_hidden_size
79
+ self.audio_projector_intermediate_size = audio_projector_intermediate_size
80
+ self.audio_projector_activation = audio_projector_activation
81
+ self.audio_projector_norm_eps = audio_projector_norm_eps
82
+ super().__init__(**kwargs)
83
+
84
+
85
+ class NemotronHAudexRMSNorm(nn.Module):
86
+ def __init__(self, hidden_size: int, eps: float = 1e-5):
87
+ super().__init__()
88
+ self.weight = nn.Parameter(torch.ones(hidden_size))
89
+ self.eps = eps
90
+
91
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
92
+ input_dtype = hidden_states.dtype
93
+ hidden_states = hidden_states.float()
94
+ variance = hidden_states.pow(2).mean(dim=-1, keepdim=True)
95
+ hidden_states = hidden_states * torch.rsqrt(variance + self.eps)
96
+ return (self.weight.float() * hidden_states).to(input_dtype)
97
+
98
+
99
+ class NemotronHAudexProjector(nn.Module):
100
+ def __init__(self, config: NemotronHAudexConfig):
101
+ super().__init__()
102
+ self.norm = NemotronHAudexRMSNorm(
103
+ config.audio_encoder_hidden_size,
104
+ eps=config.audio_projector_norm_eps,
105
+ )
106
+ self.fc1 = nn.Linear(
107
+ config.audio_encoder_hidden_size,
108
+ config.audio_projector_intermediate_size,
109
+ bias=False,
110
+ )
111
+ self.fc2 = nn.Linear(
112
+ config.audio_projector_intermediate_size,
113
+ config.hidden_size,
114
+ bias=False,
115
+ )
116
+ self.activation = config.audio_projector_activation
117
+
118
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
119
+ hidden_states = self.norm(hidden_states)
120
+ hidden_states = self.fc1(hidden_states)
121
+ if self.activation == "relu2":
122
+ hidden_states = F.relu(hidden_states).pow(2)
123
+ elif self.activation == "gelu":
124
+ hidden_states = F.gelu(hidden_states)
125
+ else:
126
+ raise ValueError(f"Unsupported audio projector activation: {self.activation}")
127
+ return self.fc2(hidden_states)
128
+
129
+
130
+ def _build_qwen2_audio_encoder(audio_config: dict) -> nn.Module:
131
+ from transformers.models.qwen2_audio.configuration_qwen2_audio import Qwen2AudioEncoderConfig
132
+ from transformers.models.qwen2_audio.modeling_qwen2_audio import Qwen2AudioEncoder
133
+
134
+ return Qwen2AudioEncoder(Qwen2AudioEncoderConfig(**dict(audio_config)))
135
+
136
+
137
+ register_checkpoint_conversion_mapping(
138
+ "NemotronHAudexForConditionalGeneration",
139
+ get_checkpoint_conversion_mapping("nemotron_h"),
140
+ overwrite=True,
141
+ )
142
+
143
+
144
+ class NemotronHAudexForConditionalGeneration(NemotronHForCausalLM):
145
+ """Audex audio modules attached to native Transformers Nemotron-H."""
146
+
147
+ config_class = NemotronHAudexConfig
148
+ _tp_plan = None
149
+ _base_model_tp_plan = None
150
+ base_model_tp_plan = None
151
+
152
+ def __init__(self, config: NemotronHAudexConfig):
153
+ super().__init__(config)
154
+ self.audio_encoder = _build_qwen2_audio_encoder(config.audio_config)
155
+ self.audio_projector = NemotronHAudexProjector(config)
156
+
157
+ def encode_audio(self, input_features: torch.Tensor) -> torch.Tensor:
158
+ encoder_param = next(self.audio_encoder.parameters())
159
+ input_features = input_features.to(device=encoder_param.device, dtype=encoder_param.dtype)
160
+ encoder_outputs = self.audio_encoder(input_features=input_features, return_dict=True)
161
+ audio_hidden = encoder_outputs.last_hidden_state
162
+ projector_param = next(self.audio_projector.parameters())
163
+ audio_hidden = audio_hidden.to(device=projector_param.device, dtype=projector_param.dtype)
164
+ return self.audio_projector(audio_hidden)
165
+
166
+ def _audio_embeddings_by_sample(
167
+ self,
168
+ input_features: Optional[torch.Tensor],
169
+ audio_embeddings: Optional[torch.Tensor],
170
+ batch_size: int,
171
+ ) -> list[torch.Tensor]:
172
+ if audio_embeddings is None:
173
+ if input_features is None:
174
+ raise ValueError("input_features or audio_embeddings must be provided for audio injection")
175
+ if input_features.ndim == 3:
176
+ projected = self.encode_audio(input_features)
177
+ if batch_size != 1:
178
+ raise ValueError(
179
+ "3D input_features represent a single sample. "
180
+ "Use 4D (batch, clips, mel_bins, frames) features for batched audio."
181
+ )
182
+ return [projected.reshape(-1, projected.shape[-1])]
183
+ if input_features.ndim == 4:
184
+ batch, clips, mel_bins, frames = input_features.shape
185
+ if batch != batch_size:
186
+ raise ValueError(f"input_features batch {batch} != input_ids batch {batch_size}")
187
+ flat_features = input_features.reshape(batch * clips, mel_bins, frames)
188
+ projected = self.encode_audio(flat_features)
189
+ projected = projected.reshape(batch, clips * projected.shape[1], projected.shape[-1])
190
+ return [projected[index] for index in range(batch)]
191
+ raise ValueError(f"Expected 3D or 4D input_features, got {tuple(input_features.shape)}")
192
+
193
+ if audio_embeddings.ndim == 2:
194
+ if batch_size != 1:
195
+ raise ValueError("2D audio_embeddings only support batch_size=1")
196
+ return [audio_embeddings.to(device=self.device)]
197
+ if audio_embeddings.ndim == 3:
198
+ if audio_embeddings.shape[0] != batch_size:
199
+ raise ValueError(f"audio_embeddings batch {audio_embeddings.shape[0]} != input_ids batch {batch_size}")
200
+ return [audio_embeddings[index].to(device=self.device) for index in range(batch_size)]
201
+ raise ValueError(f"Expected 2D or 3D audio_embeddings, got {tuple(audio_embeddings.shape)}")
202
+
203
+ def prepare_inputs_embeds(
204
+ self,
205
+ input_ids: torch.LongTensor,
206
+ input_features: Optional[torch.Tensor] = None,
207
+ audio_embeddings: Optional[torch.Tensor] = None,
208
+ ) -> torch.Tensor:
209
+ if input_ids is None:
210
+ raise ValueError("input_ids are required when injecting audio embeddings")
211
+ if self.config.sound_token_id is None:
212
+ raise ValueError("config.sound_token_id is required for audio embedding injection")
213
+
214
+ embed_device = self.model.embeddings.weight.device
215
+ input_ids = input_ids.to(embed_device)
216
+ inputs_embeds = self.model.embeddings(input_ids).clone()
217
+ audio_by_sample = self._audio_embeddings_by_sample(
218
+ input_features=input_features,
219
+ audio_embeddings=audio_embeddings,
220
+ batch_size=input_ids.shape[0],
221
+ )
222
+
223
+ for batch_index, audio in enumerate(audio_by_sample):
224
+ mask = input_ids[batch_index].to(self.device) == self.config.sound_token_id
225
+ mask = mask.to(embed_device)
226
+ expected = int(mask.sum().item())
227
+ if expected != audio.shape[0]:
228
+ raise ValueError(
229
+ "Mismatch between <so_embedding> token count and projected audio tokens: "
230
+ f"sample={batch_index} placeholders={expected} audio_tokens={audio.shape[0]}"
231
+ )
232
+ inputs_embeds[batch_index, mask] = audio.to(
233
+ device=embed_device,
234
+ dtype=inputs_embeds.dtype,
235
+ )
236
+ return inputs_embeds
237
+
238
+ @staticmethod
239
+ def _is_prefill(past_key_values) -> bool:
240
+ if past_key_values is None:
241
+ return True
242
+ get_length = getattr(past_key_values, "get_seq_length", None)
243
+ if callable(get_length):
244
+ return get_length() == 0
245
+ return len(past_key_values) == 0
246
+
247
+ def prepare_inputs_for_generation(
248
+ self,
249
+ input_ids,
250
+ past_key_values=None,
251
+ attention_mask=None,
252
+ inputs_embeds=None,
253
+ input_features=None,
254
+ audio_embeddings=None,
255
+ position_ids=None,
256
+ use_cache=True,
257
+ is_first_iteration=False,
258
+ **kwargs,
259
+ ):
260
+ prefill = self._is_prefill(past_key_values)
261
+ if prefill and inputs_embeds is None and (
262
+ input_features is not None or audio_embeddings is not None
263
+ ):
264
+ inputs_embeds = self.prepare_inputs_embeds(
265
+ input_ids=input_ids,
266
+ input_features=input_features,
267
+ audio_embeddings=audio_embeddings,
268
+ )
269
+ if prefill:
270
+ past_key_values = None
271
+ return super().prepare_inputs_for_generation(
272
+ input_ids=input_ids,
273
+ past_key_values=past_key_values,
274
+ attention_mask=attention_mask,
275
+ inputs_embeds=inputs_embeds,
276
+ position_ids=position_ids,
277
+ use_cache=use_cache,
278
+ is_first_iteration=is_first_iteration,
279
+ **kwargs,
280
+ )
281
+
282
+ def forward(
283
+ self,
284
+ input_ids: Optional[torch.LongTensor] = None,
285
+ inputs_embeds: Optional[torch.FloatTensor] = None,
286
+ input_features: Optional[torch.Tensor] = None,
287
+ audio_embeddings: Optional[torch.Tensor] = None,
288
+ **kwargs,
289
+ ):
290
+ if inputs_embeds is None and (
291
+ input_features is not None or audio_embeddings is not None
292
+ ):
293
+ inputs_embeds = self.prepare_inputs_embeds(
294
+ input_ids=input_ids,
295
+ input_features=input_features,
296
+ audio_embeddings=audio_embeddings,
297
+ )
298
+ input_ids = None
299
+ return super().forward(input_ids=input_ids, inputs_embeds=inputs_embeds, **kwargs)
inference_scripts_hf/inference_hf.py CHANGED
@@ -17,14 +17,20 @@
17
  from __future__ import annotations
18
 
19
  import argparse
 
 
20
  import json
21
  import os
22
  import shutil
23
  import sys
 
24
  from pathlib import Path
 
25
  from typing import Optional
26
 
27
  import torch
 
 
28
  from transformers import AutoConfig, AutoFeatureExtractor, AutoModelForCausalLM, AutoTokenizer
29
 
30
  SCRIPT_DIR = Path(__file__).resolve().parent
@@ -44,6 +50,8 @@ from audio_utils import (
44
  split_thinking,
45
  )
46
 
 
 
47
 
48
  def refresh_remote_code_cache(model_path: str) -> None:
49
  """Drop stale HF dynamic-module cache for this local checkpoint folder."""
@@ -65,6 +73,104 @@ def resolve_device_map(device_map: str, device: str):
65
  return device_map
66
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def load_model(
69
  model_path: str,
70
  device_map: str,
@@ -72,26 +178,41 @@ def load_model(
72
  torch_dtype: str,
73
  tp_plan: str,
74
  refresh_code_cache: bool,
 
75
  ):
76
  print("loading:", model_path)
77
- if refresh_code_cache:
 
 
78
  refresh_remote_code_cache(model_path)
79
  dtype = getattr(torch, torch_dtype) if torch_dtype != "auto" else "auto"
80
  tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
81
  print("tokenizer loaded")
82
- config = AutoConfig.from_pretrained(model_path, trust_remote_code=True)
83
- print("config loaded")
84
- feature_extractor = AutoFeatureExtractor.from_pretrained(resolve_audio_preprocessor_path(model_path, config))
85
- print("feature_extractor loaded")
 
86
  tp_plan_arg = None if tp_plan == "none" else tp_plan
87
- model = AutoModelForCausalLM.from_pretrained(
88
- model_path,
89
- trust_remote_code=True,
90
- torch_dtype=dtype,
91
- device_map=resolve_device_map(device_map, device),
92
- tp_plan=tp_plan_arg,
93
- )
 
 
 
 
 
 
 
 
 
94
  print("model loaded")
 
 
95
  model.eval()
96
  return model, tokenizer, feature_extractor, config
97
 
@@ -295,6 +416,12 @@ def parse_args() -> argparse.Namespace:
295
  action="store_true",
296
  help="Do not clear this checkpoint's stale Hugging Face dynamic-module cache before loading.",
297
  )
 
 
 
 
 
 
298
  return parser.parse_args()
299
 
300
 
@@ -307,6 +434,7 @@ def main() -> None:
307
  torch_dtype=args.torch_dtype,
308
  tp_plan=args.tp_plan,
309
  refresh_code_cache=not args.no_refresh_remote_code_cache,
 
310
  )
311
 
312
  with open(args.input_json, "r", encoding="utf-8") as f:
 
17
  from __future__ import annotations
18
 
19
  import argparse
20
+ import hashlib
21
+ import importlib.util
22
  import json
23
  import os
24
  import shutil
25
  import sys
26
+ from importlib.metadata import PackageNotFoundError, version
27
  from pathlib import Path
28
+ from types import ModuleType
29
  from typing import Optional
30
 
31
  import torch
32
+ import transformers
33
+ from packaging.version import Version
34
  from transformers import AutoConfig, AutoFeatureExtractor, AutoModelForCausalLM, AutoTokenizer
35
 
36
  SCRIPT_DIR = Path(__file__).resolve().parent
 
50
  split_thinking,
51
  )
52
 
53
+ NATIVE_TRANSFORMERS_MIN_VERSION = Version("5.14.0")
54
+
55
 
56
  def refresh_remote_code_cache(model_path: str) -> None:
57
  """Drop stale HF dynamic-module cache for this local checkpoint folder."""
 
73
  return device_map
74
 
75
 
76
+ def resolve_hf_backend(requested_backend: str) -> str:
77
+ transformers_version = Version(transformers.__version__)
78
+ if requested_backend == "legacy":
79
+ return "legacy"
80
+ if transformers_version >= NATIVE_TRANSFORMERS_MIN_VERSION:
81
+ return "native"
82
+ if requested_backend == "native":
83
+ raise RuntimeError(
84
+ f"The native backend requires transformers >= {NATIVE_TRANSFORMERS_MIN_VERSION}; "
85
+ f"found {transformers.__version__}."
86
+ )
87
+ print(
88
+ f"transformers {transformers.__version__} does not include the validated "
89
+ f"Nemotron-H cache fixes; using the slower legacy backend."
90
+ )
91
+ return "legacy"
92
+
93
+
94
+ def validate_native_kernels() -> None:
95
+ try:
96
+ from causal_conv1d import causal_conv1d_fn, causal_conv1d_update
97
+ from mamba_ssm.ops.triton.selective_state_update import selective_state_update
98
+ from mamba_ssm.ops.triton.ssd_combined import (
99
+ mamba_chunk_scan_combined,
100
+ mamba_split_conv1d_scan_combined,
101
+ )
102
+ except ImportError as exc:
103
+ raise RuntimeError(
104
+ "The native backend requires causal-conv1d==1.6.2.post1 and "
105
+ "mamba-ssm==2.3.1 built against the active PyTorch/CUDA environment."
106
+ ) from exc
107
+
108
+ kernels = (
109
+ causal_conv1d_fn,
110
+ causal_conv1d_update,
111
+ selective_state_update,
112
+ mamba_chunk_scan_combined,
113
+ mamba_split_conv1d_scan_combined,
114
+ )
115
+ if any(kernel is None for kernel in kernels):
116
+ raise RuntimeError("The native Nemotron-H fast-path kernels are unavailable.")
117
+
118
+ packages = []
119
+ for package in ("mamba-ssm", "causal-conv1d"):
120
+ try:
121
+ packages.append(f"{package}={version(package)}")
122
+ except PackageNotFoundError:
123
+ packages.append(f"{package}=unknown")
124
+ print("native kernels:", ", ".join(packages))
125
+
126
+
127
+ def load_native_module(model_path: str) -> ModuleType:
128
+ module_path = Path(model_path).resolve() / "modeling_nemotron_h_audio_native.py"
129
+ if not module_path.is_file():
130
+ raise FileNotFoundError(f"Native Audex model adapter not found: {module_path}")
131
+
132
+ path_hash = hashlib.sha256(str(module_path).encode()).hexdigest()[:12]
133
+ module_name = f"audex_native_{path_hash}"
134
+ if module_name in sys.modules:
135
+ return sys.modules[module_name]
136
+
137
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
138
+ if spec is None or spec.loader is None:
139
+ raise ImportError(f"Unable to load native Audex model adapter: {module_path}")
140
+ module = importlib.util.module_from_spec(spec)
141
+ sys.modules[module_name] = module
142
+ spec.loader.exec_module(module)
143
+ return module
144
+
145
+
146
+ def load_native_model(model_path: str, model_load_kwargs: dict):
147
+ validate_native_kernels()
148
+ native_module = load_native_module(model_path)
149
+ config_class = native_module.NemotronHAudexConfig
150
+ model_class = native_module.NemotronHAudexForConditionalGeneration
151
+
152
+ config = config_class.from_pretrained(model_path)
153
+ model, loading_info = model_class.from_pretrained(
154
+ model_path,
155
+ config=config,
156
+ output_loading_info=True,
157
+ **model_load_kwargs,
158
+ )
159
+ invalid_keys = {
160
+ key: loading_info[key]
161
+ for key in ("missing_keys", "unexpected_keys", "mismatched_keys")
162
+ if loading_info[key]
163
+ }
164
+ if invalid_keys:
165
+ raise RuntimeError(f"Native checkpoint loading was incomplete: {invalid_keys}")
166
+
167
+ from transformers.models.nemotron_h import modeling_nemotron_h
168
+
169
+ if not modeling_nemotron_h.is_fast_path_available:
170
+ raise RuntimeError("Transformers loaded without the native Nemotron-H fast path.")
171
+ return model, config
172
+
173
+
174
  def load_model(
175
  model_path: str,
176
  device_map: str,
 
178
  torch_dtype: str,
179
  tp_plan: str,
180
  refresh_code_cache: bool,
181
+ hf_backend: str,
182
  ):
183
  print("loading:", model_path)
184
+ backend = resolve_hf_backend(hf_backend)
185
+ print("hf backend:", backend)
186
+ if backend == "legacy" and refresh_code_cache:
187
  refresh_remote_code_cache(model_path)
188
  dtype = getattr(torch, torch_dtype) if torch_dtype != "auto" else "auto"
189
  tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
190
  print("tokenizer loaded")
191
+ if backend == "native":
192
+ config = None
193
+ else:
194
+ config = AutoConfig.from_pretrained(model_path, trust_remote_code=True)
195
+ print("config loaded")
196
  tp_plan_arg = None if tp_plan == "none" else tp_plan
197
+ model_load_kwargs = {
198
+ "device_map": resolve_device_map(device_map, device),
199
+ }
200
+ if tp_plan_arg is not None:
201
+ model_load_kwargs["tp_plan"] = tp_plan_arg
202
+ if backend == "native":
203
+ model_load_kwargs["dtype"] = dtype
204
+ model, config = load_native_model(model_path, model_load_kwargs)
205
+ print("config loaded")
206
+ else:
207
+ model_load_kwargs["torch_dtype"] = dtype
208
+ model = AutoModelForCausalLM.from_pretrained(
209
+ model_path,
210
+ trust_remote_code=True,
211
+ **model_load_kwargs,
212
+ )
213
  print("model loaded")
214
+ feature_extractor = AutoFeatureExtractor.from_pretrained(resolve_audio_preprocessor_path(model_path, config))
215
+ print("feature_extractor loaded")
216
  model.eval()
217
  return model, tokenizer, feature_extractor, config
218
 
 
416
  action="store_true",
417
  help="Do not clear this checkpoint's stale Hugging Face dynamic-module cache before loading.",
418
  )
419
+ parser.add_argument(
420
+ "--hf-backend",
421
+ default="auto",
422
+ choices=["auto", "native", "legacy"],
423
+ help="Use native Transformers when available, or select the reproducibility legacy path.",
424
+ )
425
  return parser.parse_args()
426
 
427
 
 
434
  torch_dtype=args.torch_dtype,
435
  tp_plan=args.tp_plan,
436
  refresh_code_cache=not args.no_refresh_remote_code_cache,
437
+ hf_backend=args.hf_backend,
438
  )
439
 
440
  with open(args.input_json, "r", encoding="utf-8") as f: