@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / c8aca0c9e1
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 7, 14:00 UTC

Support parakeet as audio encoder for nemotron-nano-vl (#35100)

Signed-off-by: Netanel Haber <58652339+netanel-haber@users.noreply.github.com> Co-authored-by: Roger Wang <hey@rogerw.io>

Netanel Haber committed Feb 27, 2026 at 20:07 UTC c8aca0c9e1b35ee4a1683a01467e638b23076a37
3 files changed +448 -20
vllm/model_executor/models/nano_nemotron_vl.py
+254 -20
@@ -44,6 +44,7 @@ from vllm.model_executor.models.internvl import (
44 )
45 from vllm.model_executor.models.module_mapping import MultiModelKeys
46 from vllm.model_executor.models.nemotron_h import NemotronHForCausalLM
47 +from vllm.model_executor.models.parakeet import ParakeetExtractor, ProjectedParakeet
48 from vllm.model_executor.models.radio import RadioModel, calc_seq_lens
49 from vllm.model_executor.models.utils import (
50 init_vllm_registered_model,
@@ -55,12 +56,14 @@ from vllm.multimodal.evs import (
56 compute_retention_mask,
57 )
58 from vllm.multimodal.inputs import (
59 + AudioItem,
60 MultiModalDataDict,
61 MultiModalFieldConfig,
62 MultiModalKwargsItems,
63 VideoItem,
64 )
65 from vllm.multimodal.parse import (
66 + AudioProcessorItems,
67 ImageEmbeddingItems,
68 ImageProcessorItems,
69 ImageSize,
@@ -91,9 +94,29 @@ Image.MAX_IMAGE_PIXELS = None # Disable the limit entirely
94 # Alternative: Set a specific higher limit
95 # Image.MAX_IMAGE_PIXELS = 300000000 # ~300M pixels
96
97 +
98 +class NanoNemotronVLAudioFeatureInputs(TensorSchema):
99 + """
100 + Dimensions:
101 + - b: Number of audio clips
102 + - t: Audio feature length
103 + - f: Feature size (mel bins)
104 + """
105 +
106 + type: Literal["audio_features"] = "audio_features"
107 + input_audio_features: Annotated[torch.Tensor, TensorShape("b", "t", "f")]
108 + feature_attention_mask: Annotated[torch.Tensor, TensorShape("b", "t")]
109 + audio_feature_lengths: Annotated[torch.Tensor, TensorShape("b")]
110 +
111 +
112 +MAX_AUDIO_LEN_S = 10 * 60 # 10 minutes
113 +
114 IMG_START = "<img>"
115 IMG_END = "</img>"
116 IMG_CONTEXT = "<image>"
117 +AUDIO_START = "<so_start>"
118 +AUDIO_END = "<so_end>"
119 +AUDIO_CONTEXT = "<so_embedding>"
120
121 # Profiling
122 # MAX_FRAMES = 16
@@ -820,6 +843,11 @@ class NanoNemotronVLProcessor(BaseNanoNemotronVLProcessor):
843 self.video_token = video_token
844 self.video_pruning_rate = video_pruning_rate
845
846 + self.audio_extractor: ParakeetExtractor | None = None
847 + raw_sound_config = getattr(config, "sound_config", None)
848 + if raw_sound_config is not None:
849 + self.audio_extractor = ParakeetExtractor(raw_sound_config)
850 +
851 # Pre-tokenize special tokens for video processing
852 # to avoid repeated tokenization
853 self._img_start_token_ids = tokenizer.encode(
@@ -952,11 +980,53 @@ class NanoNemotronVLProcessor(BaseNanoNemotronVLProcessor):
980 text = [t.replace("<video>", video_repl_text, 1) for t in text]
981 return text, video_inputs
982
983 + def _preprocess_audio(
984 + self,
985 + text: list[str],
986 + audios: list[npt.NDArray],
987 + ):
988 + if len(audios) == 0:
989 + return text, {}
990 + assert self.audio_extractor is not None
991 +
992 + extractor = self.audio_extractor
993 +
994 + parts = [x for x in re.split(f"({re.escape(AUDIO_CONTEXT)})", text[0]) if x]
995 + token_count = parts.count(AUDIO_CONTEXT)
996 + if token_count != len(audios):
997 + raise ValueError(
998 + "Number of audio tokens in text does not match the number "
999 + f"of audios (tokens={token_count}, audios={len(audios)})."
1000 + )
1001 + audio_index = 0
1002 + for idx, part in enumerate(parts):
1003 + if part == AUDIO_CONTEXT:
1004 + audio_repl = self.get_audio_repl(audios[audio_index])
1005 + parts[idx] = audio_repl.full
1006 + audio_index += 1
1007 + text = ["".join(parts)]
1008 + audio_inputs = extractor(
1009 + audios,
1010 + sampling_rate=extractor.sampling_rate,
1011 + return_tensors="pt",
1012 + )
1013 + input_audio_features = audio_inputs.input_features
1014 + feature_attention_mask = audio_inputs.attention_mask
1015 + audio_feature_lengths = feature_attention_mask.sum(dim=1)
1016 + audio_inputs = {
1017 + "input_audio_features": input_audio_features,
1018 + "feature_attention_mask": feature_attention_mask,
1019 + "audio_feature_lengths": audio_feature_lengths,
1020 + }
1021 +
1022 + return text, audio_inputs
1023 +
1024 def __call__(
1025 self,
1026 text: str | list[str] | None = None,
1027 images: Image.Image | list[Image.Image] | None = None,
1028 videos: list[tuple[npt.NDArray, dict[str, Any]]] | None = None,
1029 + audios: AudioItem | list[AudioItem] | None = None,
1030 return_tensors: str | TensorType | None = None,
1031 max_num_tiles: int | None = None,
1032 ) -> BatchFeature:
@@ -964,8 +1034,8 @@ class NanoNemotronVLProcessor(BaseNanoNemotronVLProcessor):
1034 if max_num_tiles is None:
1035 max_num_tiles = self.max_num_tiles
1036
967 - text, images, videos = [
968 - self._make_batch_input(x) for x in (text, images, videos)
1037 + text, images, videos, audios = [
1038 + self._make_batch_input(x) for x in (text, images, videos, audios)
1039 ]
1040
1041 text, image_inputs = self._preprocess_image(
@@ -980,17 +1050,22 @@ class NanoNemotronVLProcessor(BaseNanoNemotronVLProcessor):
1050 max_num_tiles=1,
1051 )
1052
1053 + text, audio_inputs = self._preprocess_audio(
1054 + text=text,
1055 + audios=audios,
1056 + )
1057 +
1058 text_inputs = self.tokenizer(text, add_special_tokens=False)
1059
1060 + combined_inputs = {**text_inputs, **video_inputs, **audio_inputs}
1061 +
1062 if self.dynamic_tiler is None:
1063 batch = BatchFeature(
987 - {**text_inputs, **video_inputs, **image_inputs},
1064 + {**combined_inputs, **image_inputs},
1065 tensor_type=return_tensors,
1066 )
1067 else:
991 - batch = BatchFeature(
992 - {**text_inputs, **video_inputs}, tensor_type=return_tensors
993 - )
1068 + batch = BatchFeature(combined_inputs, tensor_type=return_tensors)
1069 # allow images to be exempt from the BatchFeature validation:
1070 # We will .stack() them in _parse_and_validate_image_input
1071 batch.update(image_inputs)
@@ -1006,6 +1081,15 @@ class NanoNemotronVLProcessor(BaseNanoNemotronVLProcessor):
1081
1082 return PromptUpdateDetails.select_text(repl_full, IMG_CONTEXT)
1083
1084 + def get_audio_repl(
1085 + self,
1086 + audio: npt.NDArray,
1087 + ) -> PromptUpdateDetails[str]:
1088 + assert self.audio_extractor is not None
1089 + num_tokens = self.audio_extractor.audio_token_count(len(audio))
1090 + repl_full = f"{AUDIO_START}{AUDIO_CONTEXT * num_tokens}{AUDIO_END}"
1091 + return PromptUpdateDetails.select_text(repl_full, AUDIO_CONTEXT)
1092 +
1093 @classmethod
1094 def get_video_repl(
1095 cls,
@@ -1147,15 +1231,28 @@ class NanoNemotronVLProcessingInfo(BaseNanoNemotronVLProcessingInfo):
1231 def supports_video(self):
1232 return self.get_hf_processor().supports_video
1233
1234 + @property
1235 + def audio_extractor(self) -> ParakeetExtractor | None:
1236 + return self.get_hf_processor().audio_extractor
1237 +
1238 def get_data_parser(self):
1239 + target_sr = None
1240 + target_channels = None
1241 + if extractor := self.audio_extractor:
1242 + target_sr = extractor.sampling_rate
1243 + target_channels = 1
1244 +
1245 return MultiModalDataParser(
1246 video_needs_metadata=True,
1247 + target_sr=target_sr,
1248 + target_channels=target_channels,
1249 expected_hidden_size=self._get_expected_hidden_size(),
1250 )
1251
1252 def get_supported_mm_limits(self):
1253 video_limit = {"video": None} if self.supports_video else {}
1158 - return {**super().get_supported_mm_limits(), **video_limit}
1254 + audio_limit = {"audio": None} if self.audio_extractor is not None else {}
1255 + return {**super().get_supported_mm_limits(), **video_limit, **audio_limit}
1256
1257 def get_video_token(self) -> str | None:
1258 return IMG_CONTEXT
@@ -1304,7 +1401,16 @@ class NanoNemotronVLMultiModalProcessor(
1401 else:
1402 video_fields = {}
1403
1307 - return image_fields | video_fields
1404 + if self.info.audio_extractor is not None:
1405 + audio_fields = dict(
1406 + input_audio_features=MultiModalFieldConfig.batched("audio"),
1407 + feature_attention_mask=MultiModalFieldConfig.batched("audio"),
1408 + audio_feature_lengths=MultiModalFieldConfig.batched("audio"),
1409 + )
1410 + else:
1411 + audio_fields = {}
1412 +
1413 + return image_fields | video_fields | audio_fields
1414
1415 def _get_prompt_updates(
1416 self,
@@ -1373,6 +1479,20 @@ class NanoNemotronVLMultiModalProcessor(
1479 ),
1480 ]
1481
1482 + def get_audio_replacement(item_idx: int):
1483 + audios = mm_items.get_items("audio", AudioProcessorItems)
1484 + return hf_processor.get_audio_repl(audios.get(item_idx))
1485 +
1486 + if self.info.audio_extractor is not None:
1487 + prompt_repl = [
1488 + *prompt_repl,
1489 + PromptReplacement(
1490 + modality="audio",
1491 + target=AUDIO_CONTEXT,
1492 + replacement=get_audio_replacement,
1493 + ),
1494 + ]
1495 +
1496 return prompt_repl
1497
1498
@@ -1422,8 +1542,13 @@ class NanoNemotronVLDummyInputsBuilder(
1542
1543 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
1544 num_videos = mm_counts.get("video", 0)
1545 + num_audios = mm_counts.get("audio", 0)
1546
1426 - return super().get_dummy_text(mm_counts) + "<video>" * num_videos
1547 + return (
1548 + super().get_dummy_text(mm_counts)
1549 + + "<video>" * num_videos
1550 + + AUDIO_CONTEXT * num_audios
1551 + )
1552
1553 def _get_dummy_videos(
1554 self,
@@ -1482,7 +1607,25 @@ class NanoNemotronVLDummyInputsBuilder(
1607 }
1608 else:
1609 dummy_video = {}
1485 - return {**dummy_image, **dummy_video}
1610 +
1611 + if extractor := self.info.audio_extractor:
1612 + num_audios = mm_counts.get("audio", 0)
1613 + audio_overrides = mm_options.get("audio") if mm_options else None
1614 + tokens_per_audio = max(1, seq_len // max(num_audios, 1))
1615 + max_audio_num_samples = MAX_AUDIO_LEN_S * extractor.sampling_rate
1616 + calculated_max_audio_num_samples = extractor.audio_length(tokens_per_audio)
1617 + audio_len = min(max_audio_num_samples, calculated_max_audio_num_samples)
1618 + dummy_audio = {
1619 + "audio": self._get_dummy_audios(
1620 + length=audio_len,
1621 + num_audios=num_audios,
1622 + overrides=audio_overrides,
1623 + )
1624 + }
1625 + else:
1626 + dummy_audio = {}
1627 +
1628 + return {**dummy_image, **dummy_video, **dummy_audio}
1629
1630
1631 @MULTIMODAL_REGISTRY.register_processor(
@@ -1499,12 +1642,15 @@ class NemotronH_Nano_VL_V2(
1642 return "<image>"
1643 if modality.startswith("video"):
1644 return "<video>"
1645 + if modality.startswith("audio"):
1646 + return AUDIO_CONTEXT
1647 return None
1648
1649 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1650 super().__init__()
1506 - config = vllm_config.model_config.hf_config
1507 - multimodal_config = vllm_config.model_config.multimodal_config
1651 + model_config = vllm_config.model_config
1652 + config = model_config.hf_config
1653 + multimodal_config = model_config.multimodal_config
1654 image_size = config.force_image_size
1655 patch_size = config.patch_size
1656 self.patch_size = patch_size
@@ -1523,10 +1669,12 @@ class NemotronH_Nano_VL_V2(
1669 hf_config=config.text_config,
1670 prefix=maybe_prefix(prefix, "language_model"),
1671 )
1526 -
1527 - with self._mark_tower_model(vllm_config, {"image", "video"}):
1672 + llm_dtype = self.language_model.config.dtype
1673 + assert isinstance(llm_dtype, torch.dtype)
1674 + self.llm_dtype = llm_dtype
1675 + with self._mark_tower_model(vllm_config, {"image", "video", "audio"}):
1676 self.vision_model = self.get_vit_model_from_radio_config(config).to(
1529 - self.language_model.config.dtype
1677 + llm_dtype
1678 )
1679
1680 # Construct the vision projection.
@@ -1547,14 +1695,26 @@ class NemotronH_Nano_VL_V2(
1695 ReLUSquaredActivation(),
1696 nn.Linear(vision_projection_hidden_size, llm_hidden_size, bias=False),
1697 )
1550 - self.mlp1 = mlp1.to(self.language_model.config.dtype)
1698 + self.mlp1 = mlp1.to(llm_dtype)
1699 + self.sound_encoder: ProjectedParakeet | None = None
1700 + if getattr(config, "sound_config", None) is not None:
1701 + logger.info_once(
1702 + "Found sound config, initializing sound encoder for Nemotron AVLM",
1703 + scope="global",
1704 + )
1705 + self.sound_encoder = ProjectedParakeet(
1706 + config.sound_config,
1707 + dtype=llm_dtype,
1708 + llm_hidden_size=llm_hidden_size,
1709 + max_model_len=model_config.max_model_len,
1710 + )
1711
1712 self.config = config
1713 self.model_config = vllm_config.model_config
1714
1715 # Pre-tokenize special tokens for video processing
1716 # to avoid repeated tokenization
1557 - tokenizer = cached_tokenizer_from_config(vllm_config.model_config)
1717 + tokenizer = cached_tokenizer_from_config(model_config)
1718 self._img_start_token_ids = tokenizer.encode(
1719 IMG_START, add_special_tokens=False
1720 )
@@ -1566,7 +1726,10 @@ class NemotronH_Nano_VL_V2(
1726 config
1727 )
1728 if self.dynamic_resolution:
1569 - logger.info("Dynamic resolution is enabled for NanoNemotronVLProcessor")
1729 + logger.info_once(
1730 + "Dynamic resolution is enabled for NanoNemotronVLProcessor",
1731 + scope="global",
1732 + )
1733
1734 def pixel_shuffle(self, x, scale_factor=0.5):
1735 n, w, h, c = x.size()
@@ -1780,6 +1943,51 @@ class NemotronH_Nano_VL_V2(
1943
1944 return final_video_embeddings
1945
1946 + def _process_audio_input(
1947 + self, audio_input: NanoNemotronVLAudioFeatureInputs
1948 + ) -> tuple[torch.Tensor, ...]:
1949 + assert self.sound_encoder is not None
1950 + input_audio_features = audio_input.input_audio_features
1951 + feature_attention_mask = audio_input.feature_attention_mask
1952 + target_device = next(self.sound_encoder.parameters()).device
1953 +
1954 + # When cross-request batching combines audio clips with different
1955 + # time dimensions, _reduce_data returns a list instead of a stacked
1956 + # tensor. Pad to the max time dim and stack; the attention mask
1957 + # already marks valid positions so zero-padding is safe.
1958 + if isinstance(input_audio_features, list):
1959 + feature_sizes = [f.shape[-2] for f in input_audio_features]
1960 + max_t = max(feature_sizes)
1961 + padded_feats = [
1962 + torch.nn.functional.pad(feat, (0, 0, 0, max_t - feat_size))
1963 + for feat, feat_size in zip(
1964 + input_audio_features, feature_sizes, strict=True
1965 + )
1966 + ]
1967 + padded_masks = [
1968 + torch.nn.functional.pad(mask, (0, max_t - mask.shape[-1]))
1969 + for mask in feature_attention_mask
1970 + ]
1971 + input_audio_features = torch.stack(padded_feats)
1972 + feature_attention_mask = torch.stack(padded_masks)
1973 +
1974 + input_audio_features = input_audio_features.to(
1975 + dtype=self.llm_dtype, device=target_device
1976 + )
1977 + feature_attention_mask = feature_attention_mask.to(device=target_device)
1978 + sound_embeds = self.sound_encoder(input_audio_features, feature_attention_mask)
1979 +
1980 + valid_input_lens = feature_attention_mask.sum(dim=1)
1981 + valid_output_lens = self.sound_encoder.encoder._get_subsampling_output_length(
1982 + valid_input_lens
1983 + )
1984 + truncated_embeds = []
1985 + for i in range(sound_embeds.shape[0]):
1986 + valid_len = valid_output_lens[i].item()
1987 + truncated_embeds.append(sound_embeds[i, :valid_len])
1988 +
1989 + return tuple(truncated_embeds)
1990 +
1991 def _create_final_video_embeddings(
1992 self,
1993 video_embeddings: torch.Tensor,
@@ -1887,6 +2095,18 @@ class NemotronH_Nano_VL_V2(
2095 modalities["images"] = self._parse_and_validate_image_input(**kwargs)
2096 if input_key in ("pixel_values_flat_video",) and "videos" not in modalities:
2097 modalities["videos"] = self._parse_and_validate_video_input(**kwargs)
2098 + if (
2099 + input_key
2100 + in (
2101 + "input_audio_features",
2102 + "feature_attention_mask",
2103 + "audio_feature_lengths",
2104 + )
2105 + and "audios" not in modalities
2106 + ):
2107 + modalities["audios"] = NanoNemotronVLAudioFeatureInputs(
2108 + **kwargs, validate=False
2109 + )
2110
2111 return modalities
2112
@@ -1917,6 +2137,10 @@ class NemotronH_Nano_VL_V2(
2137 video_input = modalities["videos"]
2138 video_embeddings = self._process_video_input(video_input)
2139 multimodal_embeddings += tuple(video_embeddings)
2140 + if modality == "audios":
2141 + audio_input = modalities["audios"]
2142 + audio_embeddings = self._process_audio_input(audio_input)
2143 + multimodal_embeddings += tuple(audio_embeddings)
2144
2145 return multimodal_embeddings
2146
@@ -1947,8 +2171,8 @@ class NemotronH_Nano_VL_V2(
2171 """
2172 return MultiModelKeys.from_string_field(
2173 language_model="language_model",
1950 - connector="mlp1",
1951 - tower_model="vision_model",
2174 + connector=["mlp1", "sound_encoder.projection"],
2175 + tower_model=["vision_model", "sound_encoder.encoder"],
2176 )
2177
2178 def compute_logits(
@@ -1969,9 +2193,13 @@ class NemotronH_Nano_VL_V2(
2193 def is_vision_weights(name: str) -> bool:
2194 return name.startswith("vision_model.radio_model.")
2195
2196 + def is_sound_weights(name: str) -> bool:
2197 + return name.startswith("sound")
2198 +
2199 # Separate weights by component
2200 llm_weights = []
2201 vision_weights = []
2202 + sound_weights = []
2203
2204 for name, w in weights:
2205 if is_llm(name):
@@ -1987,9 +2215,15 @@ class NemotronH_Nano_VL_V2(
2215 # Convert: vision_model.radio_model.* → radio_model.*
2216 hf_key = name[len("vision_model.") :] # Remove "vision_model." prefix
2217 vision_weights.append((hf_key, w))
2218 + elif is_sound_weights(name):
2219 + assert self.sound_encoder is not None
2220 + sound_weights.append((name, w))
2221
2222 self.language_model.load_weights(llm_weights)
2223 self.vision_model.load_weights(vision_weights)
2224 + if self.sound_encoder is not None:
2225 + assert len(sound_weights) > 0
2226 + self.sound_encoder.load_weights(sound_weights)
2227
2228 def print_architecture(self, detailed: bool = True, save_to_file: str = None):
2229 """
vllm/model_executor/models/parakeet.py new
+145
@@ -0,0 +1,145 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""
4 +Modules below used for the audio encoder component in: models/nano_nemotron_vl.py
5 +"""
6 +
7 +from collections.abc import Iterable
8 +from dataclasses import asdict
9 +
10 +import numpy as np
11 +import torch
12 +import torch.nn as nn
13 +from transformers import ParakeetEncoder as HFParakeetEncoder
14 +from transformers import ParakeetFeatureExtractor, PretrainedConfig
15 +
16 +from vllm.model_executor.layers.activation import ReLUSquaredActivation
17 +from vllm.model_executor.model_loader.weight_utils import default_weight_loader
18 +from vllm.transformers_utils.configs.parakeet import ExtractorConfig, ParakeetConfig
19 +
20 +
21 +class ParakeetProjection(nn.Module):
22 + def __init__(self, config: ParakeetConfig) -> None:
23 + super().__init__()
24 + sound_hidden_size = config.hidden_size
25 + proj_hidden_size = config.projection_hidden_size
26 + llm_hidden_size = config.llm_hidden_size
27 + bias = config.projection_bias
28 +
29 + self.norm = nn.LayerNorm(sound_hidden_size, eps=config.projection_eps)
30 + self.linear1 = nn.Linear(sound_hidden_size, proj_hidden_size, bias=bias)
31 + self.activation = ReLUSquaredActivation()
32 + self.linear2 = nn.Linear(proj_hidden_size, llm_hidden_size, bias=bias)
33 +
34 + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
35 + hidden_states = self.norm(hidden_states)
36 + hidden_states = self.linear1(hidden_states)
37 + hidden_states = self.activation(hidden_states)
38 + hidden_states = self.linear2(hidden_states)
39 + return hidden_states
40 +
41 +
42 +class ProjectedParakeet(nn.Module):
43 + def __init__(
44 + self,
45 + config: PretrainedConfig,
46 + *,
47 + dtype: torch.dtype,
48 + llm_hidden_size: int,
49 + max_model_len: int,
50 + ) -> None:
51 + super().__init__()
52 + self.config = ParakeetConfig.from_hf_config(
53 + config, llm_hidden_size=llm_hidden_size, max_model_len=max_model_len
54 + )
55 + self.encoder = HFParakeetEncoder(self.config)
56 + self.encoder = self.encoder.to(dtype)
57 + self.projection = ParakeetProjection(self.config)
58 + self.projection = self.projection.to(dtype)
59 +
60 + def forward(
61 + self, input_features: torch.Tensor, attention_mask: torch.Tensor | None = None
62 + ) -> torch.Tensor:
63 + outputs = self.encoder(
64 + input_features=input_features, attention_mask=attention_mask
65 + )
66 + outputs = outputs.last_hidden_state
67 + outputs = outputs.to(dtype=torch.bfloat16)
68 + outputs = self.projection(outputs)
69 + return outputs
70 +
71 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
72 + loaded_params: set[str] = set()
73 + params_dict = dict(self.named_parameters())
74 + buffers_dict = dict(self.named_buffers())
75 +
76 + if isinstance(weights, dict):
77 + weights_list = list(weights.items())
78 + else:
79 + weights_list = list(weights)
80 +
81 + for name, weight in weights_list:
82 + if name.startswith("sound_encoder.encoder.feature_extractor."):
83 + # Feature extractor buffers are handled outside the encoder.
84 + continue
85 + if name.startswith("sound_encoder."):
86 + target_name = name[len("sound_encoder.") :]
87 + elif name.startswith("sound_projection."):
88 + target_name = f"projection.{name[len('sound_projection.') :]}"
89 + else:
90 + continue
91 +
92 + target = params_dict.get(target_name)
93 + if target is None:
94 + target = buffers_dict.get(target_name)
95 + if target is None:
96 + raise ValueError(f"Unknown weight: {name}")
97 + weight_loader = getattr(target, "weight_loader", default_weight_loader)
98 + with torch.no_grad():
99 + weight_loader(target, weight)
100 + loaded_params.add(target_name)
101 +
102 + return loaded_params
103 +
104 +
105 +class ParakeetExtractor(ParakeetFeatureExtractor):
106 + def __init__(self, config: PretrainedConfig) -> None:
107 + self.config = ExtractorConfig.from_hf_config(config)
108 + super().__init__(**asdict(self.config))
109 + self._clip_target_samples = int(
110 + round(self.config.clip_duration_s * self.sampling_rate)
111 + )
112 + self._tail_min_samples = int(
113 + round(self.config.clip_min_duration_s * self.sampling_rate)
114 + )
115 +
116 + def _normalize_audio_length(self, audio_len: int) -> int:
117 + # Match mcore's compute_params() logic for clip/minduration handling.
118 + target_len = max(audio_len, self._tail_min_samples)
119 + tail_remainder = target_len % self._clip_target_samples
120 + if 0 < tail_remainder < self._tail_min_samples:
121 + padding = self._tail_min_samples - tail_remainder
122 + target_len += padding
123 + assert isinstance(target_len, int)
124 + return target_len
125 +
126 + def audio_token_count(self, audio_len: int) -> int:
127 + audio_len = self._normalize_audio_length(audio_len)
128 + num_frames = audio_len // self.hop_length
129 + n_tokens = HFParakeetEncoder._get_subsampling_output_length(
130 + self, torch.tensor([num_frames], dtype=torch.float)
131 + )
132 + return max(1, n_tokens.item())
133 +
134 + def __call__(self, raw_speech: list[np.ndarray], *args, **kwargs):
135 + padded = []
136 + for p in raw_speech:
137 + assert p.ndim == 1
138 + audio_len = int(p.shape[0])
139 + target_len = self._normalize_audio_length(audio_len)
140 + p = np.pad(p, (0, target_len - audio_len))
141 + padded.append(p)
142 + return super().__call__(padded, *args, **kwargs)
143 +
144 + def audio_length(self, audio_tokens: int) -> int:
145 + return int(audio_tokens * self.config.subsampling_factor * self.hop_length)
vllm/transformers_utils/configs/parakeet.py new
+49
@@ -0,0 +1,49 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from dataclasses import dataclass
4 +
5 +from transformers import ParakeetEncoderConfig, PretrainedConfig
6 +
7 +
8 +class ParakeetConfig(ParakeetEncoderConfig):
9 + llm_hidden_size: int
10 + projection_hidden_size: int
11 + projection_bias: bool
12 + projection_eps: float = 1e-5
13 + sampling_rate: int
14 +
15 + @staticmethod
16 + def from_hf_config(
17 + config: PretrainedConfig, *, llm_hidden_size: int, max_model_len: int
18 + ) -> "ParakeetConfig":
19 + assert isinstance(config, PretrainedConfig)
20 + return ParakeetConfig(
21 + **config.to_dict(),
22 + scale_input=False,
23 + attention_bias=False,
24 + llm_hidden_size=llm_hidden_size,
25 + max_position_embeddings=max_model_len
26 + + 1, # + 1 because it seems like max_model_len+1 can be passed
27 + )
28 +
29 +
30 +@dataclass(kw_only=True, frozen=True)
31 +class ExtractorConfig:
32 + feature_size: int
33 + sampling_rate: int
34 + subsampling_factor: int
35 + subsampling_conv_kernel_size: int
36 + subsampling_conv_stride: int
37 + clip_duration_s: int = 30
38 + clip_min_duration_s: float = 0.1
39 +
40 + @staticmethod
41 + def from_hf_config(config: PretrainedConfig) -> "ExtractorConfig":
42 + assert isinstance(config, PretrainedConfig)
43 + return ExtractorConfig(
44 + feature_size=config.num_mel_bins,
45 + sampling_rate=config.sampling_rate,
46 + subsampling_factor=config.subsampling_factor,
47 + subsampling_conv_kernel_size=config.subsampling_conv_kernel_size,
48 + subsampling_conv_stride=config.subsampling_conv_stride,
49 + )