Download stemma/api.py from hoyso48/STEMMA-MOSS-Music: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/hoyso48/STEMMA-MOSS-Music/resolve/main/stemma/api.py
- Command line
-
hf download hf://hoyso48/STEMMA-MOSS-Music/stemma/api.py
-
curl -L -o api.py https://huggingface.co/hoyso48/STEMMA-MOSS-Music/resolve/main/stemma/api.py
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 | |
| 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 | |
| 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] | |
| 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) | |