main
py 1,135 lines 40.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Iterable, Mapping, Sequence
5 from typing import Annotated, Any, Literal, TypeAlias
6
7 import torch
8 import torch.nn as nn
9 from transformers import BatchFeature
10 from transformers.models.glmasr import GlmAsrConfig, GlmAsrProcessor
11 from transformers.models.whisper import WhisperFeatureExtractor
12
13 from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
14 from vllm.config.multimodal import MultiModalDummyOptions
15 from vllm.config.speech_to_text import SpeechToTextParams
16 from vllm.distributed.parallel_state import get_tensor_model_parallel_world_size
17 from vllm.inputs import ModalityData, MultiModalDataDict, PromptType, TokensPrompt
18 from vllm.model_executor.layers.activation import get_act_fn
19 from vllm.model_executor.layers.attention import MMEncoderAttention
20 from vllm.model_executor.layers.linear import (
21 ColumnParallelLinear,
22 QKVParallelLinear,
23 RowParallelLinear,
24 )
25 from vllm.model_executor.layers.quantization import QuantizationConfig
26 from vllm.model_executor.layers.rotary_embedding.common import ApplyRotaryEmb
27 from vllm.model_executor.models.module_mapping import MultiModelKeys
28 from vllm.multimodal import MULTIMODAL_REGISTRY
29 from vllm.multimodal.inputs import (
30 MultiModalFieldConfig,
31 MultiModalKwargsItems,
32 )
33 from vllm.multimodal.parse import (
34 DictEmbeddingItems,
35 ModalityDataItems,
36 MultiModalDataItems,
37 MultiModalDataParser,
38 )
39 from vllm.multimodal.processing import (
40 BaseDummyInputsBuilder,
41 BaseMultiModalProcessor,
42 BaseProcessingInfo,
43 PromptReplacement,
44 PromptUpdate,
45 PromptUpdateDetails,
46 )
47 from vllm.multimodal.processing.processor import HFMultiModalInputs
48 from vllm.sequence import IntermediateTensors
49 from vllm.tokenizers import cached_tokenizer_from_config
50 from vllm.transformers_utils.processor import cached_processor_from_config
51 from vllm.utils.tensor_schema import TensorSchema, TensorShape
52
53 from .glmasr_utils import (
54 DEFAULT_CONV_PARAMS,
55 DEFAULT_MAX_AUDIO_LEN_S,
56 DEFAULT_MERGE_FACTOR,
57 _flatten_audio_features_by_length,
58 _get_audio_output_lengths_for_tower,
59 _group_audio_embeddings,
60 _normalize_chunk_counts,
61 )
62 from .interfaces import (
63 MultiModalEmbeddings,
64 SupportsLoRA,
65 SupportsMultiModal,
66 SupportsPP,
67 SupportsTranscription,
68 )
69 from .utils import (
70 AutoWeightsLoader,
71 WeightsMapper,
72 init_vllm_registered_model,
73 maybe_prefix,
74 )
75 from .whisper import ISO639_1_SUPPORTED_LANGS, _create_fake_bias_for_k_proj
76
77
78 class GlmAsrEncoderRotaryEmbedding(nn.Module):
79 """Rotary Position Embedding for GLM-ASR encoder.
80
81 Computes rotary position embeddings on-demand for efficiency.
82 Only caches inv_freq as a buffer; cos/sin are computed during forward
83 to avoid wasted computation during initialization and ensure correct
84 device placement.
85 """
86
87 def __init__(self, config) -> None:
88 super().__init__()
89
90 # Compute inverse frequencies following transformers implementation
91 head_dim = getattr(
92 config, "head_dim", config.hidden_size // config.num_attention_heads
93 )
94
95 # Handle rope_parameters if present (for compatibility with transformers config)
96 if hasattr(config, "rope_parameters") and config.rope_parameters:
97 base = config.rope_parameters.get("rope_theta", 10000.0)
98 partial_rotary_factor = config.rope_parameters.get(
99 "partial_rotary_factor", 1.0
100 )
101 dim = int(head_dim * partial_rotary_factor)
102 self.attention_scaling = config.rope_parameters.get(
103 "attention_scaling", 1.0
104 )
105 else:
106 base = getattr(config, "rope_theta", 10000.0)
107 dim = head_dim
108 self.attention_scaling = 1.0
109
110 self.dim = dim
111 self.head_dim = head_dim
112
113 # Only cache inv_freq; cos/sin computed on-demand in correct device
114 inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float) / dim))
115 self.register_buffer("inv_freq", inv_freq, persistent=False)
116
117 def forward(self, seq_len: int) -> torch.Tensor:
118 """Compute rotary position frequencies for given sequence length.
119
120 Args:
121 seq_len: The sequence length to compute embeddings for.
122
123 Returns:
124 Frequency tensor with shape [seq_len, dim/2]. Use .cos() and
125 .sin() to get the rotary embedding components.
126
127 """
128 # Compute on the same device as inv_freq (automatically correct after .to())
129 seq = torch.arange(
130 seq_len, device=self.inv_freq.device, dtype=self.inv_freq.dtype
131 )
132 freqs = torch.outer(seq, self.inv_freq)
133 return freqs * self.attention_scaling
134
135
136 class GlmAsrEncoderAttention(nn.Module):
137 """Optimized Multi-headed Grouped Query Attention for GLM-ASR encoder.
138
139 Uses vLLM's QKVParallelLinear for fused projections, ApplyRotaryEmb for
140 rotary position embeddings, and MMEncoderAttention for hardware-optimized
141 attention computation with automatic backend selection.
142 """
143
144 def __init__(
145 self,
146 config,
147 quant_config: QuantizationConfig | None = None,
148 prefix: str = "",
149 ):
150 super().__init__()
151 self.config = config
152 self.hidden_size = config.hidden_size
153 self.num_heads = config.num_attention_heads
154 self.num_kv_heads = getattr(
155 config, "num_key_value_heads", config.num_attention_heads
156 )
157 self.head_dim = self.hidden_size // self.num_heads
158
159 self.tp_size = get_tensor_model_parallel_world_size()
160 self.num_heads_per_rank = self.num_heads // self.tp_size
161 self.num_kv_heads_per_rank = max(1, self.num_kv_heads // self.tp_size)
162
163 # Use QKVParallelLinear for fused QKV projection
164 # Note: GLM-ASR uses bias on Q and V, but not K
165 # For simplicity with QKVParallelLinear, we use bias=True for all
166 self.qkv_proj = QKVParallelLinear(
167 self.hidden_size,
168 self.head_dim,
169 self.num_heads,
170 self.num_kv_heads,
171 bias=True,
172 quant_config=quant_config,
173 prefix=f"{prefix}.qkv_proj",
174 )
175
176 self.o_proj = RowParallelLinear(
177 self.hidden_size,
178 self.hidden_size,
179 bias=True,
180 quant_config=quant_config,
181 prefix=f"{prefix}.o_proj",
182 )
183
184 # Use vLLM's ApplyRotaryEmb CustomOp
185 # enforce_enable=True ensures the op is always enabled (important for ViT)
186 rope_params = getattr(config, "rope_parameters", None)
187 if rope_params:
188 partial_rotary_factor = rope_params.get("partial_rotary_factor", 0.5)
189 else:
190 partial_rotary_factor = getattr(config, "partial_rotary_factor", 0.5)
191 self.rotary_dim = int(self.head_dim * partial_rotary_factor)
192 self.apply_rotary_emb = ApplyRotaryEmb(enforce_enable=True)
193
194 # Use vLLM's MMEncoderAttention for hardware-optimized attention
195 # Automatically selects Flash Attention, SDPA, or Pallas based on device
196 self.attn = MMEncoderAttention(
197 num_heads=self.num_heads_per_rank,
198 head_size=self.head_dim,
199 scale=self.head_dim**-0.5,
200 num_kv_heads=self.num_kv_heads_per_rank,
201 prefix=f"{prefix}.attn",
202 )
203
204 def forward(
205 self,
206 hidden_states: torch.Tensor,
207 rotary_pos_emb_cos: torch.Tensor,
208 rotary_pos_emb_sin: torch.Tensor,
209 ) -> torch.Tensor:
210 """Args:
211 hidden_states: [batch_size, seq_len, hidden_size]
212 rotary_pos_emb_cos: [seq_len, rotary_dim/2] - cosine of rotary embeddings
213 rotary_pos_emb_sin: [seq_len, rotary_dim/2] - sine of rotary embeddings
214
215 Returns:
216 [batch_size, seq_len, hidden_size]
217
218 """
219 batch_size, seq_len, _ = hidden_states.shape
220
221 # QKV projection - fused for efficiency
222 qkv, _ = self.qkv_proj(hidden_states)
223
224 # Split into q, k, v
225 q_size = self.num_heads_per_rank * self.head_dim
226 kv_size = self.num_kv_heads_per_rank * self.head_dim
227 q, k, v = qkv.split([q_size, kv_size, kv_size], dim=-1)
228
229 # Reshape to [batch, seq, num_heads, head_dim] for ApplyRotaryEmb
230 q = q.view(batch_size, seq_len, self.num_heads_per_rank, self.head_dim)
231 k = k.view(batch_size, seq_len, self.num_kv_heads_per_rank, self.head_dim)
232 v = v.view(batch_size, seq_len, self.num_kv_heads_per_rank, self.head_dim)
233
234 # Apply rotary position embeddings using vLLM's ApplyRotaryEmb
235 # ApplyRotaryEmb expects x: [batch, seq, heads, head_dim]
236 # cos/sin: [seq_len, rotary_dim/2]
237 q[..., : self.rotary_dim] = self.apply_rotary_emb(
238 q[..., : self.rotary_dim], rotary_pos_emb_cos, rotary_pos_emb_sin
239 )
240 k[..., : self.rotary_dim] = self.apply_rotary_emb(
241 k[..., : self.rotary_dim], rotary_pos_emb_cos, rotary_pos_emb_sin
242 )
243
244 # MMEncoderAttention expects [batch, seq, num_heads, head_dim]
245 # It handles GQA internally via repeat_interleave
246 attn_output = self.attn(q, k, v)
247
248 # Reshape back to [batch, seq, hidden_size]
249 attn_output = attn_output.view(batch_size, seq_len, -1)
250
251 # Output projection
252 output, _ = self.o_proj(attn_output)
253 return output
254
255
256 class GlmAsrEncoderMLP(nn.Module):
257 """Optimized MLP for GLM-ASR encoder.
258 Uses vLLM's parallel linear layers for better performance.
259 """
260
261 def __init__(
262 self,
263 config,
264 quant_config: QuantizationConfig | None = None,
265 prefix: str = "",
266 ):
267 super().__init__()
268 self.config = config
269 self.hidden_size = config.hidden_size
270 self.intermediate_size = config.intermediate_size
271
272 self.fc1 = ColumnParallelLinear(
273 self.hidden_size,
274 self.intermediate_size,
275 bias=True,
276 quant_config=quant_config,
277 prefix=f"{prefix}.fc1",
278 )
279
280 self.act_fn = get_act_fn(config.hidden_act)
281
282 self.fc2 = RowParallelLinear(
283 self.intermediate_size,
284 self.hidden_size,
285 bias=True,
286 quant_config=quant_config,
287 prefix=f"{prefix}.fc2",
288 )
289
290 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
291 hidden_states, _ = self.fc1(hidden_states)
292 hidden_states = self.act_fn(hidden_states)
293 hidden_states, _ = self.fc2(hidden_states)
294 return hidden_states
295
296
297 class GlmAsrEncoderLayer(nn.Module):
298 """Optimized Transformer encoder layer for GLM-ASR.
299 Combines attention and MLP with residual connections and layer norms.
300 """
301
302 def __init__(
303 self,
304 config,
305 quant_config: QuantizationConfig | None = None,
306 prefix: str = "",
307 ):
308 super().__init__()
309 self.hidden_size = config.hidden_size
310
311 self.self_attn = GlmAsrEncoderAttention(
312 config,
313 quant_config=quant_config,
314 prefix=f"{prefix}.self_attn",
315 )
316
317 self.mlp = GlmAsrEncoderMLP(
318 config,
319 quant_config=quant_config,
320 prefix=f"{prefix}.mlp",
321 )
322
323 layer_norm_eps = getattr(config, "layer_norm_eps", 1e-5)
324 self.input_layernorm = nn.LayerNorm(self.hidden_size, eps=layer_norm_eps)
325 self.post_attention_layernorm = nn.LayerNorm(
326 self.hidden_size, eps=layer_norm_eps
327 )
328
329 def forward(
330 self,
331 hidden_states: torch.Tensor,
332 rotary_pos_emb_cos: torch.Tensor,
333 rotary_pos_emb_sin: torch.Tensor,
334 ) -> torch.Tensor:
335 """Args:
336 hidden_states: [batch_size, seq_len, hidden_size]
337 rotary_pos_emb_cos: [seq_len, rotary_dim/2] - cosine of rotary embeddings
338 rotary_pos_emb_sin: [seq_len, rotary_dim/2] - sine of rotary embeddings
339
340 Returns:
341 [batch_size, seq_len, hidden_size]
342
343 """
344 # Self-attention with residual
345 residual = hidden_states
346 hidden_states = self.input_layernorm(hidden_states)
347 hidden_states = self.self_attn(
348 hidden_states=hidden_states,
349 rotary_pos_emb_cos=rotary_pos_emb_cos,
350 rotary_pos_emb_sin=rotary_pos_emb_sin,
351 )
352 hidden_states = residual + hidden_states
353
354 # MLP with residual
355 residual = hidden_states
356 hidden_states = self.post_attention_layernorm(hidden_states)
357 hidden_states = self.mlp(hidden_states)
358 hidden_states = residual + hidden_states
359
360 return hidden_states
361
362
363 class _GlmAsrEncoderOutput:
364 """Simple output container compatible with transformers' BaseModelOutput.
365
366 This lightweight container holds the encoder output and is compatible
367 with the transformers library's output format while being more efficient
368 than a full dataclass.
369
370 Attributes:
371 last_hidden_state: Final layer hidden states from the encoder.
372 Shape: [batch_size, seq_len, hidden_size]
373
374 """
375
376 __slots__ = ("last_hidden_state",)
377
378 def __init__(self, last_hidden_state: torch.Tensor):
379 self.last_hidden_state = last_hidden_state
380
381
382 class GlmAsrEncoder(nn.Module):
383 """Optimized GLM-ASR Audio Encoder with vLLM native implementation.
384
385 This encoder processes audio features through convolutional layers
386 followed by transformer layers with rotary position embeddings.
387 Optimized for performance with:
388 - QKVParallelLinear for fused attention projections
389 - Tensor parallelism support via ColumnParallelLinear/RowParallelLinear
390 - Quantization support
391 - Flash Attention (SDPA)
392 """
393
394 # Mapping for weight loading: transformers uses separate q/k/v, we use fused qkv
395 packed_modules_mapping = {
396 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
397 }
398
399 hf_to_vllm_mapper = WeightsMapper(
400 orig_to_new_stacked={
401 ".q_proj": (".qkv_proj", "q"),
402 ".k_proj": (".qkv_proj", "k"),
403 ".v_proj": (".qkv_proj", "v"),
404 }
405 )
406
407 def __init__(
408 self,
409 config,
410 quant_config: QuantizationConfig | None = None,
411 prefix: str = "",
412 ):
413 super().__init__()
414 self.config = config
415
416 # Convolutional feature extraction layers
417 self.conv1 = nn.Conv1d(
418 config.num_mel_bins,
419 config.hidden_size,
420 kernel_size=3,
421 padding=1,
422 )
423 self.conv2 = nn.Conv1d(
424 config.hidden_size,
425 config.hidden_size,
426 kernel_size=3,
427 stride=2,
428 padding=1,
429 )
430
431 # Transformer encoder layers
432 self.layers = nn.ModuleList(
433 [
434 GlmAsrEncoderLayer(
435 config,
436 quant_config=quant_config,
437 prefix=f"{prefix}.layers.{layer_idx}",
438 )
439 for layer_idx in range(config.num_hidden_layers)
440 ]
441 )
442
443 # Final layer norm
444 layer_norm_eps = getattr(config, "layer_norm_eps", 1e-5)
445 self.norm = nn.LayerNorm(config.hidden_size, eps=layer_norm_eps)
446
447 # Rotary position embeddings
448 self.rotary_emb = GlmAsrEncoderRotaryEmbedding(config)
449
450 def _get_feat_extract_output_lengths(
451 self, input_lengths: torch.Tensor
452 ) -> tuple[torch.Tensor, torch.Tensor]:
453 """Compute the output length after convolutions.
454
455 Args:
456 input_lengths: Input sequence lengths [batch_size]
457
458 Returns:
459 Tuple of (output after conv1, output after conv2)
460
461 """
462 # Conv1: kernel=3, stride=1, padding=1
463 output_lengths_conv1 = (input_lengths + 2 * 1 - 3) // 1 + 1
464
465 # Conv2: kernel=3, stride=2, padding=1
466 output_lengths_conv2 = (output_lengths_conv1 + 2 * 1 - 3) // 2 + 1
467
468 return output_lengths_conv1, output_lengths_conv2
469
470 def forward(self, input_features: torch.Tensor) -> _GlmAsrEncoderOutput:
471 """Forward pass through the encoder.
472
473 Args:
474 input_features: [batch_size, num_mel_bins, seq_len]
475
476 Returns:
477 _GlmAsrEncoderOutput: Object with .last_hidden_state attribute \
478 containing [batch_size, seq_len', hidden_size] where seq_len' \
479 is the sequence length after convolutions
480
481 """
482 # Apply convolutional layers with GELU activation
483 hidden_states = torch.nn.functional.gelu(self.conv1(input_features))
484 hidden_states = torch.nn.functional.gelu(self.conv2(hidden_states))
485
486 # Transpose to [batch_size, seq_len, hidden_size]
487 hidden_states = hidden_states.transpose(1, 2)
488 output_seq_len = hidden_states.shape[1]
489
490 # Compute rotary position embeddings on-demand
491 rotary_pos_emb = self.rotary_emb(output_seq_len)
492 rotary_pos_emb_cos = rotary_pos_emb.cos().to(dtype=hidden_states.dtype)
493 rotary_pos_emb_sin = rotary_pos_emb.sin().to(dtype=hidden_states.dtype)
494
495 # Apply transformer layers
496 for encoder_layer in self.layers:
497 hidden_states = encoder_layer(
498 hidden_states, rotary_pos_emb_cos, rotary_pos_emb_sin
499 )
500
501 # Final layer norm
502 hidden_states = self.norm(hidden_states)
503
504 # Return in a format compatible with transformers' BaseModelOutput
505 return _GlmAsrEncoderOutput(last_hidden_state=hidden_states)
506
507 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
508 weights = _create_fake_bias_for_k_proj(weights, ".k_proj.weight")
509 loader = AutoWeightsLoader(self)
510 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
511
512
513 class GlmAsrFeatureInputs(TensorSchema):
514 """Dimensions:
515 - num_chunks: Number of audio chunks (flattened)
516 - nmb: Number of mel bins
517 - num_audios: Number of original audio files
518 """
519
520 type: Literal["audio_features"]
521 input_features: Annotated[
522 torch.Tensor | list[torch.Tensor],
523 TensorShape("num_chunks", "nmb", "chunk_length", dynamic_dims={"chunk_length"}),
524 ]
525 feature_attention_mask: Annotated[
526 torch.Tensor | list[torch.Tensor],
527 TensorShape("num_chunks", "chunk_length", dynamic_dims={"chunk_length"}),
528 ]
529 chunk_counts: Annotated[
530 torch.Tensor | list[torch.Tensor],
531 TensorShape("num_audios"),
532 ]
533
534
535 class GlmAsrEmbeddingInputs(TensorSchema):
536 """Dimensions:
537 - bn: Batch size
538 - naf: Number of audio features
539 - hs: Hidden size (must match the hidden size of language model
540 backbone)
541 """
542
543 type: Literal["audio_embeds"] = "audio_embeds"
544 audio_embeds: Annotated[
545 list[torch.Tensor],
546 TensorShape("bn", "naf", "hs", dynamic_dims={"naf"}),
547 ]
548
549
550 GlmAsrInputs: TypeAlias = GlmAsrFeatureInputs | GlmAsrEmbeddingInputs
551
552
553 class GlmAsrMultiModalProjector(nn.Module):
554 """Projects audio encoder outputs to language model hidden space.
555
556 This projector uses a two-layer MLP to map audio features from the
557 encoder's intermediate size to the language model's hidden size.
558 Uses vLLM's parallel linear layers for tensor parallelism support.
559
560 Architecture:
561 - Linear layer: intermediate_size -> hidden_size * 2
562 - Activation function (e.g., GELU)
563 - Linear layer: hidden_size * 2 -> hidden_size
564 """
565
566 def __init__(
567 self,
568 config: GlmAsrConfig,
569 quant_config: QuantizationConfig | None = None,
570 prefix: str = "",
571 ):
572 super().__init__()
573 self.linear_1 = ColumnParallelLinear(
574 input_size=config.audio_config.intermediate_size,
575 output_size=config.text_config.hidden_size * 2,
576 quant_config=quant_config,
577 prefix=f"{prefix}.linear_1",
578 )
579 self.act = get_act_fn(config.projector_hidden_act)
580 self.linear_2 = RowParallelLinear(
581 input_size=config.text_config.hidden_size * 2,
582 output_size=config.text_config.hidden_size,
583 quant_config=quant_config,
584 prefix=f"{prefix}.linear_2",
585 )
586
587 def forward(self, audio_features: torch.Tensor) -> torch.Tensor:
588 hidden_states, _ = self.linear_1(audio_features)
589 hidden_states = self.act(hidden_states)
590 hidden_states, _ = self.linear_2(hidden_states)
591 return hidden_states
592
593
594 def _glmasr_field_config(
595 hf_inputs: Mapping[str, torch.Tensor],
596 ) -> dict[str, MultiModalFieldConfig]:
597 """Configure multimodal field batching strategy for GLM-ASR.
598
599 Determines how to batch audio inputs based on whether chunking is used.
600 When chunk_counts is present, features are flattened across chunks;
601 otherwise, they are batched normally.
602
603 Args:
604 hf_inputs: Dictionary of preprocessed inputs from HuggingFace processor.
605
606 Returns:
607 Dictionary mapping field names to MultiModalFieldConfig objects \
608 that specify batching behavior.
609
610 """
611 chunk_counts = hf_inputs.get("chunk_counts")
612 if chunk_counts is not None:
613 return dict(
614 audio_embeds=MultiModalFieldConfig.batched("audio"),
615 input_features=MultiModalFieldConfig.flat_from_sizes(
616 "audio", chunk_counts, dim=0
617 ),
618 feature_attention_mask=MultiModalFieldConfig.flat_from_sizes(
619 "audio", chunk_counts, dim=0
620 ),
621 chunk_counts=MultiModalFieldConfig.batched("audio", keep_on_cpu=True),
622 )
623 return dict(
624 audio_embeds=MultiModalFieldConfig.batched("audio"),
625 input_features=MultiModalFieldConfig.batched("audio"),
626 feature_attention_mask=MultiModalFieldConfig.batched("audio"),
627 chunk_counts=MultiModalFieldConfig.batched("audio", keep_on_cpu=True),
628 )
629
630
631 class GlmAsrMultiModalDataParser(MultiModalDataParser):
632 """Custom parser for GLM-ASR multimodal data.
633
634 Extends the base parser to handle GLM-ASR specific audio data formats,
635 including both pre-computed audio embeddings and raw audio features.
636 """
637
638 def _parse_audio_data(
639 self,
640 data: dict[str, torch.Tensor] | ModalityData[Any],
641 ) -> ModalityDataItems[Any, Any] | None:
642 if isinstance(data, dict):
643 return DictEmbeddingItems(
644 data,
645 modality="audio",
646 required_fields={"audio_embeds"},
647 fields_factory=_glmasr_field_config,
648 )
649 return super()._parse_audio_data(data)
650
651
652 class GlmAsrProcessingInfo(BaseProcessingInfo):
653 """Processing information provider for GLM-ASR model.
654
655 Provides access to model configuration, processor, and feature extractor
656 needed for audio preprocessing and multimodal integration.
657 """
658
659 def get_hf_config(self) -> GlmAsrConfig:
660 return self.ctx.get_hf_config(GlmAsrConfig)
661
662 def get_hf_processor(self, **kwargs: object) -> GlmAsrProcessor:
663 return self.ctx.get_hf_processor(GlmAsrProcessor, **kwargs)
664
665 def get_feature_extractor(self, **kwargs: object) -> WhisperFeatureExtractor:
666 return self.get_hf_processor(**kwargs).feature_extractor
667
668 def get_data_parser(self):
669 feature_extractor = self.get_feature_extractor()
670
671 return GlmAsrMultiModalDataParser(
672 target_sr=feature_extractor.sampling_rate,
673 expected_hidden_size=self._get_expected_hidden_size(),
674 )
675
676 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
677 return {"audio": None}
678
679
680 class GlmAsrDummyInputsBuilder(BaseDummyInputsBuilder[GlmAsrProcessingInfo]):
681 """Builder for dummy inputs used in profiling and testing.
682
683 Generates dummy text prompts and audio data that match the expected
684 format for GLM-ASR model inputs. Used for memory profiling and
685 performance benchmarking.
686 """
687
688 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
689 num_audios = mm_counts.get("audio", 0)
690 hf_processor = self.info.get_hf_processor()
691 return hf_processor.audio_token * num_audios
692
693 def get_dummy_mm_data(
694 self,
695 seq_len: int,
696 mm_counts: Mapping[str, int],
697 mm_options: MultiModalDummyOptions,
698 ) -> MultiModalDataDict:
699 feature_extractor = self.info.get_feature_extractor()
700 sampling_rate = feature_extractor.sampling_rate
701
702 max_audio_len = getattr(
703 self.info.get_hf_processor(), "max_audio_len", DEFAULT_MAX_AUDIO_LEN_S
704 )
705 audio_len = int(max_audio_len * sampling_rate)
706
707 return {
708 "audio": self._get_dummy_audios(
709 length=audio_len,
710 num_audios=mm_counts.get("audio", 0),
711 overrides=mm_options.get("audio"),
712 )
713 }
714
715
716 class GlmAsrMultiModalProcessor(BaseMultiModalProcessor["GlmAsrProcessingInfo"]):
717 """GLM-ASR processor that inherits directly from BaseMultiModalProcessor
718 for better performance and cleaner implementation.
719 """
720
721 def _calculate_chunk_counts(
722 self,
723 audio_list: list[Any],
724 feature_extractor: WhisperFeatureExtractor,
725 processor: GlmAsrProcessor,
726 ) -> list[int]:
727 sampling_rate = feature_extractor.sampling_rate
728 chunk_length = feature_extractor.chunk_length
729 max_audio_len = getattr(processor, "max_audio_len", DEFAULT_MAX_AUDIO_LEN_S)
730 window_size = int(sampling_rate * chunk_length)
731 max_windows = int(max_audio_len // chunk_length)
732
733 chunk_counts = []
734 for audio in audio_list:
735 n_samples = len(audio) if isinstance(audio, list) else audio.shape[0]
736 n_chunks = max(1, (n_samples + window_size - 1) // window_size)
737 chunk_counts.append(min(n_chunks, max_windows))
738 return chunk_counts
739
740 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
741 return self.dummy_inputs.get_dummy_text(mm_counts)
742
743 def _get_hf_mm_inputs(
744 self,
745 mm_items: MultiModalDataItems,
746 hf_kwargs: Mapping[str, object],
747 ) -> HFMultiModalInputs:
748 hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
749
750 feature_extractor = self.info.get_feature_extractor(**hf_kwargs)
751 return hf_inputs._replace(
752 hf_kwargs=dict(
753 hf_inputs.hf_kwargs,
754 sampling_rate=feature_extractor.sampling_rate,
755 )
756 )
757
758 def _postprocess_hf_mm_data(
759 self,
760 mm_data: Mapping[str, object],
761 hf_processor_mm_kwargs: Mapping[str, object],
762 processed_data: BatchFeature,
763 ) -> BatchFeature:
764 # Handle different key names from different transformers versions
765 if "input_features_mask" in processed_data:
766 processed_data["feature_attention_mask"] = processed_data.pop(
767 "input_features_mask"
768 )
769 elif "input_features" in processed_data:
770 # If no mask is provided, create one from input_features
771 input_features = processed_data["input_features"]
772 if isinstance(input_features, torch.Tensor):
773 # Create a mask of all ones matching the sequence length
774 mask = torch.ones(
775 input_features.shape[0],
776 input_features.shape[-1],
777 dtype=torch.long,
778 )
779 processed_data["feature_attention_mask"] = mask
780
781 audio = mm_data.get("audio")
782 if audio is None:
783 audio_list: list[Any] = []
784 elif isinstance(audio, list):
785 audio_list = audio
786 else:
787 audio_list = [audio]
788 if audio_list:
789 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
790
791 # Override chunk counts calculation with GLM-ASR specific logic
792 chunk_counts = self._calculate_chunk_counts(
793 audio_list, processor.feature_extractor, processor
794 )
795 processed_data["chunk_counts"] = torch.tensor(
796 chunk_counts, dtype=torch.long
797 )
798
799 return processed_data
800
801 def _get_mm_fields_config(
802 self,
803 hf_inputs: BatchFeature,
804 hf_processor_mm_kwargs: Mapping[str, object],
805 ) -> Mapping[str, MultiModalFieldConfig]:
806 return _glmasr_field_config(hf_inputs)
807
808 def _get_prompt_updates(
809 self,
810 mm_items: MultiModalDataItems,
811 hf_processor_mm_kwargs: Mapping[str, object],
812 out_mm_kwargs: MultiModalKwargsItems,
813 ) -> Sequence[PromptUpdate]:
814 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
815 tokenizer = self.info.get_tokenizer()
816 vocab = tokenizer.get_vocab()
817 config = self.info.get_hf_config()
818
819 audio_token = getattr(processor, "audio_token", "<|pad|>")
820 audio_token_id = vocab.get(audio_token)
821 if audio_token_id is None:
822 audio_token_id = processor.audio_token_id
823
824 merge_factor = getattr(config, "merge_factor", DEFAULT_MERGE_FACTOR)
825 conv_params = getattr(config, "conv_params", DEFAULT_CONV_PARAMS)
826 out_mm_data = out_mm_kwargs.get_data()
827 feature_attention_mask = out_mm_data.get("feature_attention_mask")
828 chunk_counts = out_mm_data.get("chunk_counts")
829
830 # Pre-compute audio output lengths if feature_attention_mask is available
831 audio_output_lengths: list[int] = []
832 if feature_attention_mask is not None:
833 # Compute output lengths for all audio items
834 from .glmasr_utils import (
835 _as_list_chunk_counts,
836 _get_audio_output_lengths_from_mask,
837 )
838
839 if chunk_counts is not None:
840 start_idx = 0
841 for count in _as_list_chunk_counts(chunk_counts):
842 end_idx = start_idx + count
843 mask = feature_attention_mask[start_idx:end_idx]
844 if isinstance(mask, list):
845 mask = torch.stack(mask)
846
847 lengths = _get_audio_output_lengths_from_mask(
848 mask, merge_factor, conv_params
849 )
850 audio_output_lengths.append(int(lengths.sum().item()))
851 start_idx = end_idx
852 else:
853 # Single chunk per audio
854 for idx in range(len(feature_attention_mask)):
855 mask = feature_attention_mask[idx : idx + 1]
856 if isinstance(mask, list):
857 mask = torch.tensor(mask).unsqueeze(0)
858 lengths = _get_audio_output_lengths_from_mask(
859 mask, merge_factor, conv_params
860 )
861 audio_output_lengths.append(int(lengths.sum().item()))
862
863 def get_replacement_glmasr(item_idx: int):
864 # Use pre-computed lengths if available, otherwise fall back to audio_embeds
865 if audio_output_lengths:
866 num_features = audio_output_lengths[item_idx]
867 else:
868 audio_embeds = out_mm_data.get("audio_embeds")
869 if audio_embeds is not None:
870 embed = audio_embeds[item_idx]
871 assert isinstance(embed, torch.Tensor)
872 num_features = embed.shape[0]
873 else:
874 raise ValueError(
875 "Either feature_attention_mask or audio_embeds must be provided"
876 )
877
878 if num_features == 0:
879 raise ValueError("Audio is too short")
880
881 audio_tokens = [audio_token_id] * int(num_features)
882 return PromptUpdateDetails.select_token_id(
883 audio_tokens,
884 embed_token_id=audio_token_id,
885 )
886
887 return [
888 PromptReplacement(
889 modality="audio",
890 target=[audio_token_id],
891 replacement=get_replacement_glmasr,
892 )
893 ]
894
895
896 @MULTIMODAL_REGISTRY.register_processor(
897 GlmAsrMultiModalProcessor,
898 info=GlmAsrProcessingInfo,
899 dummy_inputs=GlmAsrDummyInputsBuilder,
900 )
901 class GlmAsrForConditionalGeneration(
902 nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA, SupportsTranscription
903 ):
904 supported_languages = ISO639_1_SUPPORTED_LANGS
905
906 hf_to_vllm_mapper = WeightsMapper(
907 orig_to_new_prefix={"audio_tower.embed_positions": None}
908 )
909
910 packed_modules_mapping = {
911 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
912 "gate_up_proj": ["gate_proj", "up_proj"],
913 }
914
915 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
916 super().__init__()
917 config = vllm_config.model_config.hf_config
918 quant_config = vllm_config.quant_config
919 multimodal_config = vllm_config.model_config.multimodal_config
920 self.config = config
921 self.multimodal_config = multimodal_config
922 self.quant_config = quant_config
923
924 with self._mark_tower_model(vllm_config, "audio"):
925 self.audio_tower = GlmAsrEncoder(
926 config.audio_config,
927 quant_config=quant_config,
928 prefix=maybe_prefix(prefix, "audio_tower"),
929 )
930 self.multi_modal_projector = GlmAsrMultiModalProjector(
931 config,
932 quant_config=quant_config,
933 prefix=maybe_prefix(prefix, "multi_modal_projector"),
934 )
935
936 with self._mark_language_model(vllm_config):
937 self.language_model = init_vllm_registered_model(
938 vllm_config=vllm_config,
939 hf_config=config.text_config,
940 prefix=maybe_prefix(prefix, "language_model"),
941 architectures=["LlamaForCausalLM"],
942 )
943
944 self.make_empty_intermediate_tensors = (
945 self.language_model.make_empty_intermediate_tensors
946 )
947
948 @classmethod
949 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
950 if modality.startswith("audio"):
951 return "<|begin_of_audio|><|pad|><|end_of_audio|>"
952
953 raise ValueError("Only audio modality is supported")
954
955 def get_mm_mapping(self) -> MultiModelKeys:
956 return MultiModelKeys.from_string_field(
957 language_model="language_model.",
958 connector="multi_modal_projector.",
959 tower_model="audio_tower.",
960 )
961
962 def _parse_and_validate_audio_input(self, **kwargs: object) -> GlmAsrInputs | None:
963 audio_embeds = kwargs.pop("audio_embeds", None)
964 if audio_embeds is not None:
965 return GlmAsrEmbeddingInputs(type="audio_embeds", audio_embeds=audio_embeds)
966
967 input_features = kwargs.pop("input_features", None)
968 if input_features is None:
969 return None
970
971 return GlmAsrFeatureInputs(
972 type="audio_features",
973 input_features=input_features,
974 feature_attention_mask=kwargs.pop("feature_attention_mask", None),
975 chunk_counts=kwargs.pop("chunk_counts", None),
976 )
977
978 def _process_audio_input(
979 self, audio_input: GlmAsrInputs
980 ) -> torch.Tensor | tuple[torch.Tensor, ...]:
981 if audio_input["type"] == "audio_embeds":
982 return tuple(audio_input["audio_embeds"])
983
984 input_features = audio_input["input_features"]
985 feature_attention_mask = audio_input["feature_attention_mask"]
986
987 if isinstance(input_features, list):
988 input_features = torch.cat(input_features, dim=0)
989 feature_attention_mask = torch.cat(feature_attention_mask, dim=0)
990
991 num_chunks = input_features.shape[0]
992 chunk_counts = _normalize_chunk_counts(
993 audio_input.get("chunk_counts"), num_chunks=num_chunks
994 )
995
996 # Convert input_features to model dtype (e.g., bfloat16) to match model weights
997 input_features = input_features.to(dtype=self.audio_tower.conv1.weight.dtype)
998
999 # audio_tower returns [batch_size, seq_len, hidden_size] where hidden_size=1280
1000 audio_hidden_states = self.audio_tower(input_features).last_hidden_state
1001
1002 # GLM-ASR merges consecutive frames: 4 frames with hidden_size=1280
1003 # -> 1 frame with intermediate_size=5120
1004 hidden_size = self.config.audio_config.hidden_size
1005 intermediate_size = self.config.audio_config.intermediate_size
1006 merge_ratio = intermediate_size // hidden_size
1007
1008 # Truncate sequence length to be divisible by merge_ratio
1009 seq_len = audio_hidden_states.shape[1]
1010 seq_len_truncated = (seq_len // merge_ratio) * merge_ratio
1011 if seq_len_truncated < seq_len:
1012 audio_hidden_states = audio_hidden_states[:, :seq_len_truncated, :]
1013
1014 # Reshape to merge consecutive frames
1015 audio_hidden_states = audio_hidden_states.reshape(
1016 num_chunks,
1017 -1,
1018 intermediate_size,
1019 )
1020
1021 audio_features = self.multi_modal_projector(audio_hidden_states)
1022
1023 merge_factor = getattr(self.config, "merge_factor", DEFAULT_MERGE_FACTOR)
1024 conv_params = getattr(self.config, "conv_params", DEFAULT_CONV_PARAMS)
1025
1026 audio_output_lengths = _get_audio_output_lengths_for_tower(
1027 self.audio_tower,
1028 feature_attention_mask.sum(-1),
1029 merge_factor,
1030 conv_params,
1031 )
1032
1033 masked_audio_features = _flatten_audio_features_by_length(
1034 audio_features, audio_output_lengths
1035 )
1036
1037 chunk_embeddings = torch.split(
1038 masked_audio_features, audio_output_lengths.flatten().tolist()
1039 )
1040 return _group_audio_embeddings(chunk_embeddings, chunk_counts)
1041
1042 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
1043 audio_input = self._parse_and_validate_audio_input(**kwargs)
1044 if audio_input is None:
1045 return []
1046
1047 masked_audio_features = self._process_audio_input(audio_input)
1048
1049 return masked_audio_features
1050
1051 def forward(
1052 self,
1053 input_ids: torch.Tensor | None,
1054 positions: torch.Tensor,
1055 intermediate_tensors: IntermediateTensors | None = None,
1056 inputs_embeds: torch.Tensor | None = None,
1057 **kwargs: object,
1058 ) -> torch.Tensor | IntermediateTensors:
1059 if intermediate_tensors is not None:
1060 inputs_embeds = None
1061
1062 hidden_states = self.language_model.model(
1063 input_ids,
1064 positions,
1065 intermediate_tensors,
1066 inputs_embeds=inputs_embeds,
1067 )
1068 return hidden_states
1069
1070 def compute_logits(
1071 self,
1072 hidden_states: torch.Tensor,
1073 ) -> torch.Tensor | None:
1074 return self.language_model.compute_logits(hidden_states)
1075
1076 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1077 loader = AutoWeightsLoader(self)
1078 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1079
1080 @classmethod
1081 def _get_audio_token(cls, model_config: ModelConfig) -> str:
1082 """Get the audio token from processor.
1083
1084 Similar to get_placeholder_str but returns single token.
1085 """
1086 processor = cached_processor_from_config(model_config)
1087 return getattr(processor, "audio_token", "<|pad|>")
1088
1089 @classmethod
1090 def get_speech_to_text_config(
1091 cls, model_config: ModelConfig, task_type: str
1092 ) -> SpeechToTextConfig:
1093 processor = cached_processor_from_config(model_config)
1094 feature_extractor = processor.feature_extractor
1095 max_audio_clip_s = getattr(processor, "max_audio_len", DEFAULT_MAX_AUDIO_LEN_S)
1096 return SpeechToTextConfig(
1097 max_audio_clip_s=max_audio_clip_s,
1098 sample_rate=feature_extractor.sampling_rate,
1099 )
1100
1101 @classmethod
1102 def get_generation_prompt(cls, stt_params: SpeechToTextParams) -> PromptType:
1103 """Get the generation prompt to be used for transcription requests."""
1104 audio = stt_params.audio
1105 model_config = stt_params.model_config
1106 task_type = stt_params.task_type
1107 to_language = stt_params.to_language
1108 tokenizer = cached_tokenizer_from_config(model_config)
1109 audio_token = cls._get_audio_token(model_config)
1110
1111 if task_type == "translate":
1112 full_lang_name_to = (
1113 cls.supported_languages.get(to_language, to_language)
1114 if to_language is not None
1115 else ""
1116 )
1117 user_content = f"{audio_token}translate the speech to {full_lang_name_to}"
1118 elif task_type == "transcribe":
1119 user_content = (
1120 f"{audio_token}can you transcribe the speech into a written format?"
1121 )
1122 else:
1123 raise ValueError(f"Unsupported task type {task_type}")
1124
1125 messages = [{"role": "user", "content": user_content}]
1126 prompt = tokenizer.apply_chat_template(
1127 messages, tokenize=False, add_generation_prompt=True
1128 )
1129
1130 prompt_token_ids = tokenizer.encode(prompt)
1131
1132 return TokensPrompt(
1133 prompt_token_ids=prompt_token_ids,
1134 multi_modal_data={"audio": audio},
1135 )