hoyso48's picture
Simplify model card and repository files
ff30113 verified
Raw History Blame Contribute Delete
14.6 kB
"""Small native API; model objects remain editable and hookable PyTorch modules."""
from contextlib import nullcontext
from dataclasses import dataclass
import json
from pathlib import Path
from typing import Sequence
import torch
from .audio import Request, load_audio, normalize_prompt
@dataclass
class AudioSlot:
"""One ordered source; token_positions [U_i] index the padded input row."""
sample_index: int
audio_index: int
token_positions: torch.Tensor
samples_16k: int
windows: int | None
@dataclass
class PreparedBatch:
"""Ready-to-forward tensors, validated user prompt strings and source slots.
inputs contains input_ids/attention_mask [B,L]. MF adds mel [W,128,3000]
and frame mask [W,3000]. MOSS adds mel [A,128,F_max], lengths [A],
audio_input_mask [B,L], audio_group_sizes [B], audio_token_lens [A].
All tensors live on the model device; floating features use model dtype.
slots identify valid audio-token positions, never padding or timestamp digits.
"""
inputs: dict[str, torch.Tensor]
slots: list[AudioSlot]
prompts: list[str]
windows_per_source: list[int]
@dataclass
class AudioEmbedding:
"""One source's unpooled token features, with padding already removed.
encoder [U_i,1280], projected [U_i,D], token_positions [U_i]. D=3584
for MF, 4096 for MOSS. MOSS has three deepstack_encoder [U_i,1280] and
deepstack_projected [U_i,4096] tensors; MF has empty DeepStack tuples.
Encoder != projected != language-contextual representations.
"""
sample_index: int
audio_index: int
token_positions: torch.Tensor
encoder: torch.Tensor
projected: torch.Tensor
deepstack_encoder: tuple[torch.Tensor, ...]
deepstack_projected: tuple[torch.Tensor, ...]
class Session:
"""Single-device inference session; not a training or concurrent-serving API.
model is the actual torch module (not a remote text client). Users may edit
its forward or attach hooks. Default methods disable gradients; grad=True
enables ordinary autograd for feature/forward research without checkpointed
training. Do not invoke the same MOSS instance concurrently: DeepStack hooks
are scoped to one forward and removed on success or failure.
"""
def __init__(self, model, processor, family: str):
if family not in {"mf2601", "moss_music"}:
raise ValueError("Require mf2601 or moss_music")
self.model = model
self.processor = processor
self.family = family
if family == "mf2601":
from . import mf2601 as backend
self.context_length = int(model.config.text_config.max_position_embeddings)
else:
from . import moss_music as backend
self.context_length = int(model.config.language_config.max_position_embeddings)
self.backend = backend
def prepare(self, requests: Sequence[Request]) -> PreparedBatch:
"""Decode/expand B single-turn requests, return tensors and ordered slots.
Reject marker mismatches, empty/nonfinite waveforms, unsupported native
tokens and prompts leaving no output context. No truncation of tokens,
source concatenation, deduplication or sorting is performed.
"""
if not requests:
raise ValueError("At least one Request is required")
prompts = [normalize_prompt(r.prompt, len(r.audios), label_single_audio=self.family != "mf2601") for r in requests]
groups = [[load_audio(a, crop_seconds=1200 if self.family == "mf2601" else None) for a in r.audios] for r in requests]
inputs, lens, windows = self.backend.prepare(prompts, groups, self.processor)
if inputs["input_ids"].shape[1] >= self.context_length:
raise ValueError("Expanded multimodal prompt leaves no output context")
device, dtype = self.model.device, self.model.dtype
inputs = {k: v.to(device=device, dtype=dtype if v.is_floating_point() else v.dtype) for k, v in inputs.items()}
token_id = self.model.config.audio_token_id if self.family == "mf2601" else self.processor.audio_token_id
slots = []
source_index = 0
for sample_index, group in enumerate(groups):
positions = torch.where((inputs["input_ids"][sample_index] == token_id) & inputs["attention_mask"][sample_index].bool())[0]
offset = 0
for audio_index, audio in enumerate(group):
count = lens[source_index]
source_positions = positions[offset:offset + count]
if len(source_positions) != count or count <= 0:
raise ValueError("Audio marker expansion differs from source token lengths")
slots.append(AudioSlot(sample_index, audio_index, source_positions, len(audio), windows[source_index] if windows else None))
offset += count
source_index += 1
if offset != len(positions):
raise ValueError("Unexpected extra audio tokens in expanded input")
if source_index != len(lens):
raise ValueError("Some source lengths were not consumed")
return PreparedBatch(inputs, slots, prompts, windows)
def _autocast(self):
if self.model.device.type == "cuda" and self.model.dtype in {torch.bfloat16, torch.float16}:
return torch.autocast("cuda", dtype=self.model.dtype)
return nullcontext()
def forward(self, batch: PreparedBatch, *, output_hidden_states: bool = False, grad: bool = False):
"""Return native ModelOutput: logits [B,1,V], optionally hidden [B,L,D].
Runs prefill only with cache disabled. Hidden-state index 0 is embedded
input; 1..K correspond to decoder-layer boundaries (last includes final
normalization). Audio positions are language-contextual features, not raw
audio. Full hidden states can be expensive for long prompts.
"""
with torch.set_grad_enabled(grad), self._autocast():
return self.model(**batch.inputs, use_cache=False, return_dict=True,
output_hidden_states=output_hidden_states, logits_to_keep=1)
def extract_audio_embeddings(self, batch: PreparedBatch, *, grad: bool = False) -> list[AudioEmbedding]:
"""Encode sources without running the LLM; return one AudioEmbedding/slot.
See AudioEmbedding for exact shapes. No mean pooling/detach is applied;
grad=True preserves the autograd graph through encoder and projectors.
"""
if not batch.slots:
return []
with torch.set_grad_enabled(grad), self._autocast():
raw, projected, deep_raw, deep_projected = self.backend.audio_features(self.model, batch.inputs, batch.windows_per_source)
total = sum(len(s.token_positions) for s in batch.slots)
if raw.shape[0] != total or projected.shape[0] != total:
raise ValueError("Feature lengths differ from expanded source token counts")
if any(s.shape[0] != total for s in (*deep_raw, *deep_projected)):
raise ValueError("DeepStack token count differs from final encoder stream")
results = []
offset = 0
for slot in batch.slots:
stop = offset + len(slot.token_positions)
results.append(AudioEmbedding(slot.sample_index, slot.audio_index, slot.token_positions,
raw[offset:stop], projected[offset:stop], tuple(s[offset:stop] for s in deep_raw),
tuple(s[offset:stop] for s in deep_projected)))
offset = stop
return results
def extract_contextual_embeddings(self, batch: PreparedBatch, layers: Sequence[int] = (-1,), *, grad: bool = False):
"""Return list[dict] per source: requested layer -> tensor [U_i,D].
Uses forward output_hidden_states, including MOSS DeepStack injection.
token_positions refer to the padded input row. Negative layer indexes
follow Python indexing; invalid indexes fail, never silently clamp.
"""
if not layers:
raise ValueError("At least one hidden-state layer is required")
outputs = self.forward(batch, output_hidden_states=True, grad=grad)
if outputs.hidden_states is None:
raise RuntimeError("Native model did not return language hidden states")
return [{"sample_index": s.sample_index, "audio_index": s.audio_index,
"token_positions": s.token_positions,
"layers": {i: outputs.hidden_states[i][s.sample_index, s.token_positions] for i in layers}}
for s in batch.slots]
def generate(self, requests: Sequence[Request], *, profile: str = "greedy", max_new_tokens: int = 16384) -> list[str]:
"""Return one generated string/request using KV cache; no retry/rewrite.
greedy decodes deterministically; sampled uses the base model's sampling
defaults. In a batch, the token budget is capped by the longest prompt;
use batch size 1 near the context limit.
"""
if type(max_new_tokens) is not int or max_new_tokens < 1:
raise ValueError("max_new_tokens must be a positive integer")
if profile not in {"greedy", "sampled"}:
raise ValueError("Unknown generation profile")
batch = self.prepare(requests)
width = batch.inputs["input_ids"].shape[1]
budget = min(max_new_tokens, self.context_length - width)
from transformers import GenerationConfig
original = self.model.generation_config
sampled = ({"temperature": 0.7 if self.family == "mf2601" else 1.0,
"top_p": 0.9 if self.family == "mf2601" else 0.8, "top_k": 50}
if profile == "sampled" else {})
config = GenerationConfig(
do_sample=profile == "sampled", max_new_tokens=budget,
repetition_penalty=1.0,
num_beams=1, num_return_sequences=1, use_cache=True,
bos_token_id=original.bos_token_id, eos_token_id=original.eos_token_id,
pad_token_id=self.processor.tokenizer.pad_token_id,
**sampled,
)
# fork_rng restores the caller's RNG state; a sampled batch uses one RNG stream.
devices = [self.model.device] if self.model.device.type == "cuda" else []
with torch.random.fork_rng(devices=devices), torch.no_grad(), self._autocast():
torch.manual_seed(3407)
output = self.model.generate(**batch.inputs, generation_config=config, logits_to_keep=1)
return [s.strip() for s in self.processor.batch_decode(output[:, width:], skip_special_tokens=True)]
def load_model(model_path: str | Path, *, device: str = "cuda:0", dtype: torch.dtype = torch.bfloat16,
attention: str = "sdpa") -> Session:
"""Load a downloaded model folder (e.g. from snapshot_download).
The returned Session exposes .model and .processor. No remote code or global
patches are used. Missing, unexpected or mismatched tensors fail explicitly.
"""
path = Path(model_path).expanduser()
if not path.is_dir():
raise FileNotFoundError("Pass a local model directory")
if attention not in {"sdpa", "eager"}:
raise ValueError("Supported attention: sdpa or eager")
cfg = json.loads((path / "config.json").read_text())
kwargs = dict(local_files_only=True, dtype=dtype, attn_implementation=attention, output_loading_info=True)
if cfg.get("architectures") == ["MusicFlamingoForConditionalGeneration"]:
from transformers import AutoProcessor, MusicFlamingoForConditionalGeneration
processor = AutoProcessor.from_pretrained(path, local_files_only=True)
model, loading = MusicFlamingoForConditionalGeneration.from_pretrained(path, **kwargs)
from .mf2601 import patch_zero_token_window_timestamps
patch_zero_token_window_timestamps(model)
family = "mf2601"
elif cfg.get("architectures") == ["MossMusicModel"]:
from transformers import Qwen2Tokenizer
from .vendor.configuration_moss_music import MossMusicConfig
from .vendor.modeling_moss_music import MossMusicModel
from .vendor.processing_moss_music import MelConfig, MossMusicProcessor
from .moss_patches import (patch_moss_music_batch_forward, patch_moss_music_audio_encoder_official_chunks,
patch_whisper_encoder_layer_outputs, reset_moss_music_sinusoid_buffer)
class AttrDict(dict):
def __getattr__(self, key):
try:
return self[key]
except KeyError as exc:
raise AttributeError(key) from exc
markers = {cfg.get("audio_encoder_chunking"), cfg["audio_config"].get("audio_encoder_chunking")}
if markers != {"official_chunked_400"}:
raise ValueError("Expected a checkpoint trained with official 400-frame audio-encoder chunking")
config = MossMusicConfig.from_dict(cfg)
config.audio_config = AttrDict(config.audio_config)
# The final encoder used eager attention; LLM SDPA is an inference backend
# choice. Do not apply the persisted FA4 training backend to the encoder.
config.audio_config["_attn_implementation"] = "eager"
config.language_config._attn_implementation = attention
config._attn_implementation = attention
tokenizer = Qwen2Tokenizer.from_pretrained(path, local_files_only=True)
processor = MossMusicProcessor(tokenizer, mel_config=MelConfig(), enable_time_marker=True)
model, loading = MossMusicModel.from_pretrained(path, config=config, **kwargs)
reset_moss_music_sinusoid_buffer(model)
patch_whisper_encoder_layer_outputs(model)
patch_moss_music_audio_encoder_official_chunks(model)
patch_moss_music_batch_forward(model)
family = "moss_music"
else:
raise ValueError("Unknown architecture; refuse to guess the model family")
failures = {k: loading.get(k) for k in ("missing_keys", "unexpected_keys", "mismatched_keys", "error_msgs") if loading.get(k)}
if failures:
raise RuntimeError(f"Incomplete checkpoint loading: {failures}")
model.to(device).eval()
# Moving BF16 modules would downcast the nonpersistent sinusoid buffer.
# Restore after placement too, without changing protected config files.
if family == "moss_music":
reset_moss_music_sinusoid_buffer(model)
model.config.use_cache = True
return Session(model, processor, family)