"""Create a tiny random `qwen3_asr` (Qwen3-ForcedAligner) fixture.
Produces a ``Qwen3ASRForConditionalGeneration`` whose top-level ``model_type`` is
``qwen3_asr`` and whose ``thinker_config.model_type`` is ``qwen3_forced_aligner``
(so the model builds the forced-aligner ``classify_num`` head instead of the
vocabulary head), wrapping a ``qwen3_asr_audio_encoder`` audio tower and a
``qwen3_asr_text`` decoder.
The tiny artifact preserves the real architecture. Weights are random; the
original 0.6B checkpoint is never downloaded.
Requirements: the third-party ``qwen_asr`` package (registers the trust-remote-code
architecture with ``AutoConfig``/``AutoModel``), transformers, torch, soundfile.
Usage:
python qwen3_forced_aligner_tiny_model_generation.py
# writes ./tiny-random-qwen3-forced-aligner next to this script.
"""
import json
import os
import sys
from pathlib import Path
import numpy as np
import torch
# qwen_asr must be imported to register the model with AutoConfig/AutoModel.
import qwen_asr # noqa: F401
from transformers import AutoConfig, AutoProcessor
from qwen_asr.core.transformers_backend.modeling_qwen3_asr import Qwen3ASRForConditionalGeneration
SEED = 42
MODEL_ID = "Qwen/Qwen3-ForcedAligner-0.6B"
OUTPUT_DIR = Path(__file__).resolve().parent / "tiny-random-qwen3-forced-aligner"
# The forced-aligner path is selected by the instance model_type of the thinker
# sub-config. ``PretrainedConfig.to_dict`` serializes the class attribute
# ("qwen3_asr_thinker"), so the saved config.json must be patched back to this
# value for the reloaded model to keep the classify head.
FORCED_ALIGNER_MODEL_TYPE = "qwen3_forced_aligner"
MAX_PARAMS = 25_000_000
MAX_BYTES = 100 * 1024 * 1024
def register_default_rope_shim():
"""Compatibility shim for transformers >= 5.
The ``qwen_asr`` remote code targets transformers 4.57, where the
``"default"`` RoPE type is an entry in ``ROPE_INIT_FUNCTIONS``. Transformers 5
dropped that dict entry (turning it into a rotary-embedding method), so the
remote code's ``ROPE_INIT_FUNCTIONS[self.rope_type]`` lookup and the tf5
weight-init path both fail for ``rope_type == "default"``. Re-register an
equivalent classic-RoPE initializer and attach it to the remote rotary
modules under the name tf5 expects.
"""
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
from qwen_asr.core.transformers_backend import modeling_qwen3_asr as mod
def _compute_default_rope_parameters(config, device=None, seq_len=None, **kwargs):
rope_params = getattr(config, "rope_parameters", None) or getattr(config, "rope_scaling", None) or {}
base = rope_params.get("rope_theta") if isinstance(rope_params, dict) else None
if base is None:
base = getattr(config, "rope_theta", 10000.0)
dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads
inv_freq = 1.0 / (
base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim)
)
return inv_freq, 1.0
ROPE_INIT_FUNCTIONS.setdefault("default", _compute_default_rope_parameters)
# tf5 `_init_weights` calls `module.compute_default_rope_parameters` on every
# rotary module whose `rope_type == "default"`.
for name in dir(mod):
obj = getattr(mod, name)
if isinstance(obj, type) and name.endswith("RotaryEmbedding"):
if not hasattr(obj, "compute_default_rope_parameters"):
obj.compute_default_rope_parameters = staticmethod(_compute_default_rope_parameters)
def build_config():
config = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
thinker = config.thinker_config
# Preserve the forced-aligner identity + classify head width.
assert "forced_aligner" in thinker.model_type, thinker.model_type
text = thinker.text_config
audio = thinker.audio_config
# transformers 5 dropped the generic special-token attributes from the base
# PretrainedConfig; the remote code still reads config.pad_token_id.
if not hasattr(thinker, "pad_token_id"):
thinker.pad_token_id = None
# --- Text decoder (qwen3_asr_text): shrink transformer, keep tokenizer vocab. ---
text.hidden_size = 64
text.head_dim = 16
text.num_attention_heads = 4
text.num_key_value_heads = 2
text.intermediate_size = 128
text.num_hidden_layers = 2
text.max_position_embeddings = 2048
# Keep the interleaved M-RoPE path; rescale mrope_section to sum to head_dim // 2.
# Original invariant: sum([24, 20, 20]) == 64 == original head_dim(128) // 2.
rope_scaling = dict(text.rope_scaling)
assert rope_scaling["mrope_interleaved"] is True, rope_scaling
assert sum(rope_scaling["mrope_section"]) == 128 // 2, rope_scaling["mrope_section"]
rope_scaling["mrope_section"] = [4, 2, 2] # sum == 8 == head_dim(16) // 2
text.rope_scaling = rope_scaling
# --- Audio encoder (qwen3_asr_audio_encoder): shrink conformer, keep mel bins. ---
audio.d_model = 64
audio.encoder_layers = 2
audio.num_hidden_layers = 2
audio.encoder_attention_heads = 4 # head_dim = d_model / heads = 16
audio.encoder_ffn_dim = 128
audio.downsample_hidden_size = 32
# output_dim must match the text hidden size so audio embeddings merge into
# the decoder embedding space.
audio.output_dim = 64
# num_mel_bins stays 128: the processor always emits 128 mel-bin features.
# Force float32 at every effective precision field.
for sub in (config, thinker, text, audio):
sub.dtype = "float32"
sub.torch_dtype = "float32"
return config
def patch_saved_thinker_model_type(output_dir: Path) -> None:
"""Restore the forced-aligner thinker model_type in the saved config.json."""
config_path = output_dir / "config.json"
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
data.setdefault("thinker_config", {})["model_type"] = FORCED_ALIGNER_MODEL_TYPE
with open(config_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
def normalize_tied_weight_keys(model) -> None:
"""Convert legacy list-format ``_tied_weights_keys`` to the tf5 dict format.
The ``qwen_asr`` remote code declares ``_tied_weights_keys`` as a list (the
transformers 4.x format), but tf5's ``save_pretrained`` calls ``.keys()`` on
it. Nothing is actually tied here (``tie_word_embeddings=False`` and the
forced-aligner ``lm_head`` differs in shape from the embeddings), so an empty
mapping is correct.
"""
for module in model.modules():
keys = getattr(module, "_tied_weights_keys", None)
if isinstance(keys, list):
module._tied_weights_keys = {}
def generate_audio_data(seconds: float = 2.0, sample_rate: int = 16000):
np.random.seed(SEED)
t = np.linspace(0, 1.0, int(sample_rate * seconds), endpoint=False)
audio = (0.5 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)
return audio, sample_rate
def run_inference(output_dir: Path) -> None:
"""Reload the exact saved artifact and run a forward + short generate."""
reloaded_config = AutoConfig.from_pretrained(output_dir, trust_remote_code=True)
assert reloaded_config.model_type == "qwen3_asr", reloaded_config.model_type
assert "forced_aligner" in reloaded_config.thinker_config.model_type, reloaded_config.thinker_config.model_type
assert reloaded_config.thinker_config.audio_config.model_type == "qwen3_asr_audio_encoder"
assert reloaded_config.thinker_config.text_config.model_type == "qwen3_asr_text"
model = Qwen3ASRForConditionalGeneration.from_pretrained(output_dir, trust_remote_code=True).float().eval()
# The classify head width must equal classify_num for a forced aligner.
classify_num = reloaded_config.thinker_config.classify_num
assert model.thinker.lm_head.out_features == classify_num, (
model.thinker.lm_head.out_features,
classify_num,
)
processor = AutoProcessor.from_pretrained(output_dir, trust_remote_code=True)
audio_data, sample_rate = generate_audio_data()
text_prompt = processor.apply_chat_template(
[
{"role": "system", "content": ""},
{"role": "user", "content": [{"type": "audio", "audio": ""}]},
],
add_generation_prompt=True,
tokenize=False,
)
inputs = processor(text=text_prompt, audio=audio_data, sampling_rate=sample_rate, return_tensors="pt")
with torch.no_grad():
outputs = model.thinker(
input_ids=inputs["input_ids"],
input_features=inputs["input_features"],
feature_attention_mask=inputs["feature_attention_mask"],
attention_mask=inputs["attention_mask"],
)
logits = outputs.logits
print(f" forward ok, logits shape: {tuple(logits.shape)} (last dim == classify_num={classify_num})")
assert logits.shape[-1] == classify_num
with torch.no_grad():
generated = model.generate(
input_ids=inputs["input_ids"],
input_features=inputs["input_features"],
feature_attention_mask=inputs["feature_attention_mask"],
attention_mask=inputs["attention_mask"],
max_new_tokens=5,
do_sample=False,
)
if hasattr(generated, "sequences"):
generated = generated.sequences
print(f" generate ok, output shape: {tuple(generated.shape)}")
def main():
torch.manual_seed(SEED)
register_default_rope_shim()
config = build_config()
model = Qwen3ASRForConditionalGeneration(config).float().eval()
num_params = sum(p.numel() for p in model.parameters())
num_bytes = sum(p.numel() * p.element_size() for p in model.parameters())
print(f"params={num_params:,} bytes={num_bytes:,} ({num_bytes / 1024 / 1024:.2f} MB)")
if num_params > MAX_PARAMS:
sys.exit(f"param budget exceeded: {num_params:,} > {MAX_PARAMS:,}")
if num_bytes > MAX_BYTES:
sys.exit(f"memory budget exceeded: {num_bytes:,} > {MAX_BYTES:,}")
normalize_tied_weight_keys(model)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
model.save_pretrained(OUTPUT_DIR, safe_serialization=True)
processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
processor.save_pretrained(OUTPUT_DIR)
patch_saved_thinker_model_type(OUTPUT_DIR)
print(f"Saved tiny qwen3 forced aligner model to: {OUTPUT_DIR}")
run_inference(OUTPUT_DIR)
print("Inference smoke test passed.")
if __name__ == "__main__":
main()
- Downloads last month
- 18
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support