Text Generation
Transformers
Safetensors
sumi
mask-generation
diffusion
discrete-diffusion
uniform-diffusion
custom_code
Instructions to use tohoku-nlp/sumi-7b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use tohoku-nlp/sumi-7b with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="tohoku-nlp/sumi-7b", trust_remote_code=True)# Load model directly from transformers import AutoModelForMaskGeneration model = AutoModelForMaskGeneration.from_pretrained("tohoku-nlp/sumi-7b", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use tohoku-nlp/sumi-7b with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "tohoku-nlp/sumi-7b" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tohoku-nlp/sumi-7b", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/tohoku-nlp/sumi-7b
- SGLang
How to use tohoku-nlp/sumi-7b with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "tohoku-nlp/sumi-7b" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tohoku-nlp/sumi-7b", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "tohoku-nlp/sumi-7b" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tohoku-nlp/sumi-7b", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use tohoku-nlp/sumi-7b with Docker Model Runner:
docker model run hf.co/tohoku-nlp/sumi-7b
| # coding=utf-8 | |
| # Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Iterative denoising sampler for the uniform-only GIDD diffusion language model. | |
| For uniform-only GIDD the noise distribution is constant ``π = 1/V`` and the | |
| forward marginal is:: | |
| q(z_t = v | x) = α_t · 1[v = x] + (1 - α_t) / V | |
| Given a model that predicts ``x̂(z_t)``, ``z_s`` (with ``s < t`` in the denoising | |
| direction) is sampled from the analytic ancestral posterior:: | |
| p(z_s = v | z_t, x̂) ∝ q(z_s = v | x̂) · q(z_t | z_s = v) | |
| This is the Hugging Face port of ``tools/run_uniform_diffusion_sampling.py``; | |
| the same two samplers (``"ancestral"`` and ``"adaptive"``) are exposed through | |
| :meth:`SumiGenerationMixin.generate`. | |
| """ | |
| import math | |
| from dataclasses import dataclass | |
| from typing import Callable, List, Optional, Tuple | |
| import torch | |
| import torch.nn.functional as F | |
| from transformers.cache_utils import Cache | |
| from transformers.generation import GenerationConfig, GenerationMixin | |
| from transformers.utils import ModelOutput | |
| class SumiGenerationOutput(ModelOutput): | |
| """Output class for uniform diffusion generation.""" | |
| sequences: torch.LongTensor | |
| past_key_values: Cache | None = None | |
| logits: None = None | |
| hidden_states: None = None | |
| canvas: torch.LongTensor | None = None # full untrimmed denoised canvas | |
| class SumiGenerationConfig(GenerationConfig): | |
| """Generation configuration for uniform diffusion language models.""" | |
| def __init__( | |
| self, | |
| num_denoising_steps: int = 128, | |
| sampler: str = "ancestral", | |
| schedule: str = "linear", | |
| min_log_snr: float = -9.0, | |
| max_log_snr: float = 9.0, | |
| tokens_per_step: int = 1, | |
| **kwargs, | |
| ): | |
| super().__init__(**kwargs) | |
| if num_denoising_steps < 1: | |
| raise ValueError("num_denoising_steps must be a positive integer.") | |
| self.num_denoising_steps = num_denoising_steps | |
| self.sampler = sampler | |
| self.schedule = schedule | |
| self.min_log_snr = min_log_snr | |
| self.max_log_snr = max_log_snr | |
| self.tokens_per_step = tokens_per_step | |
| def from_model_config(cls, model_config): | |
| # transformers>=5.12's GenerationConfig.from_model_config compares the generation | |
| # config against a base GenerationConfig() that lacks this subclass's custom fields | |
| # (num_denoising_steps, sampler, ...), raising AttributeError. Build via the base | |
| # class, then re-wrap so our extra defaults are preserved. Robust on 5.8-5.12+. | |
| base = GenerationConfig.from_model_config(model_config) | |
| return cls(**base.to_dict()) | |
| def _make_log_snr_schedule( | |
| num_steps: int, | |
| schedule: str, | |
| min_log_snr: float, | |
| max_log_snr: float, | |
| device: torch.device, | |
| ) -> torch.Tensor: | |
| """``[num_steps + 1]`` log-SNR values, ascending (noisy → clean).""" | |
| if schedule == "linear": | |
| alpha_ts = torch.linspace(1e-4, 1.0 - 1e-4, num_steps + 1, device=device) | |
| elif schedule == "cosine": | |
| ts = torch.linspace(1.0 - 1e-3, 1e-3, num_steps + 1, device=device) | |
| alpha_ts = 0.5 + 0.5 * torch.cos(ts * math.pi) | |
| else: | |
| raise ValueError(f"Unknown schedule: {schedule!r}") | |
| alpha_ts = alpha_ts.clamp(min=1e-6, max=1.0 - 1e-6) | |
| log_snrs = torch.log(alpha_ts) - torch.log1p(-alpha_ts) | |
| return log_snrs.clamp(min=min_log_snr, max=max_log_snr) | |
| def _compute_logits( | |
| model, | |
| z: torch.Tensor, | |
| attention_mask: torch.Tensor, | |
| vocab_size: int, | |
| ) -> torch.Tensor: | |
| """Forward pass → raw ``[B, S, vocab_size]`` logits truncated to the real vocab. | |
| Entries beyond ``vocab_size`` are dropped (matching how training masks padded | |
| vocab); returned as fp32. Shared by both the ancestral path (which softmaxes | |
| with temperature, via :func:`_compute_x_hat`) and the adaptive path (which | |
| needs the raw logits to separate the un-tempered selection distribution from | |
| the tempered commit distribution). | |
| """ | |
| logits = model( | |
| input_ids=z, | |
| attention_mask=attention_mask, | |
| use_cache=False, | |
| ).logits | |
| return logits[..., :vocab_size].float() | |
| def _compute_x_hat( | |
| model, | |
| z: torch.Tensor, | |
| attention_mask: torch.Tensor, | |
| vocab_size: int, | |
| temperature: float = 1.0, | |
| ) -> torch.Tensor: | |
| """Forward pass → ``softmax(logits / temperature)`` truncated to the real vocab. | |
| ``temperature`` sharpens (``< 1``) or flattens (``> 1``) the model's predicted | |
| clean-token distribution before it feeds the analytic ancestral posterior. | |
| ``1.0`` reproduces full posterior sampling; lower values make denoising more | |
| decisive. The ancestral step that samples ``z_s`` from the posterior keeps its | |
| own inherent stochasticity either way. | |
| """ | |
| logits = _compute_logits(model, z, attention_mask, vocab_size) | |
| if temperature != 1.0: | |
| logits = logits / max(float(temperature), 1e-6) | |
| return F.softmax(logits, dim=-1) | |
| def _ancestral_step( | |
| z_t: torch.Tensor, | |
| x_hat: torch.Tensor, | |
| log_snr_t: float, | |
| log_snr_s: float, | |
| vocab_size: int, | |
| generator: Optional[torch.Generator], | |
| eps: float = 1e-12, | |
| ) -> torch.Tensor: | |
| """One ancestral denoising step ``z_t → z_s`` (with ``s < t``). | |
| Shapes: ``z_t [B, S]``, ``x_hat [B, S, V]``. Returns ``[B, S]``. | |
| """ | |
| device = z_t.device | |
| dtype = x_hat.dtype | |
| alpha_t = torch.sigmoid(torch.tensor(log_snr_t, device=device, dtype=dtype)) | |
| alpha_s = torch.sigmoid(torch.tensor(log_snr_s, device=device, dtype=dtype)) | |
| alpha_t_s = alpha_t / alpha_s.clamp(min=eps) | |
| beta_t = 1.0 - alpha_t | |
| beta_s = 1.0 - alpha_s | |
| beta_t_s = (1.0 - alpha_t_s).clamp(min=0.0) | |
| inv_v = 1.0 / vocab_size | |
| u_t = beta_t * inv_v | |
| u_s = beta_s * inv_v | |
| u_t_s = beta_t_s * inv_v | |
| q_s = alpha_s * x_hat + u_s | |
| one_hot_zt = F.one_hot(z_t, num_classes=vocab_size).to(dtype) | |
| q_t_given_s = alpha_t_s * one_hot_zt + u_t_s | |
| x_hat_at_zt = x_hat.gather(-1, z_t.unsqueeze(-1)).squeeze(-1) | |
| q_t_at_zt = (alpha_t * x_hat_at_zt + u_t).clamp(min=eps) | |
| posterior = q_s * q_t_given_s / q_t_at_zt.unsqueeze(-1) | |
| posterior = posterior.clamp(min=0.0) | |
| flat = posterior.reshape(-1, vocab_size) | |
| sampled = torch.multinomial(flat, num_samples=1, generator=generator).squeeze(-1) | |
| return sampled.view_as(z_t) | |
| def _adaptive_step( | |
| z_t: torch.Tensor, | |
| logits: torch.Tensor, | |
| noise_mask: torch.Tensor, | |
| tokens_per_step: int, | |
| temperature: float, | |
| generator: Optional[torch.Generator], | |
| ) -> Tuple[torch.Tensor, torch.Tensor]: | |
| """One confidence-based ("adaptive") denoising step ``z_t → z_s``. | |
| Implements the generalized confidence heuristic of von Rütte et al. (ICLR 2026, | |
| "Scaling Behavior of Discrete Diffusion Language Models", App. A.1, Eq. 9): | |
| conf(z_t) = p_prior(z_t) · ( max_z' p_θ(x=z'|z_t) − p_θ(x=z_t|z_t) ) | |
| For the UNIFORM-only model the prior is constant ``π = 1/V`` over the whole vocab | |
| (there is no mask token), so it drops out of the ranking — the confidence reduces | |
| to ``p_max − p_curr`` ("how much the model wants to change this token"). | |
| Unlike :func:`_ancestral_step`, which resamples EVERY denoise position each step, | |
| this commits ONLY the top ``tokens_per_step`` highest-confidence denoise positions | |
| to their predicted token (argmax when ``temperature == 0``, else sampled from | |
| ``softmax(logits/temperature)``) and leaves all other positions unchanged. The SNR | |
| schedule is unused, so more steps than tokens are allowed — a position may be | |
| revisited and overwritten on a later step. | |
| Shapes: ``z_t [B, S]``, ``logits [B, S, V]`` (un-tempered, fp32), | |
| ``noise_mask [B, S]`` (True = denoise here, False = frozen prompt/anchor). | |
| Returns ``(z_s [B, S], selected_pos [B, k])``. Frozen positions are guaranteed | |
| unchanged. | |
| """ | |
| vocab_size = logits.shape[-1] | |
| # Selection distribution: un-tempered softmax (the confidence compares the model's | |
| # raw belief in the best token vs. the current token). | |
| x_hat = F.softmax(logits, dim=-1) | |
| p_max = x_hat.max(dim=-1).values # [B, S] | |
| p_curr = x_hat.gather(-1, z_t.unsqueeze(-1)).squeeze(-1) # [B, S] | |
| conf = p_max - p_curr # >= 0 (uniform prior const) | |
| # Never pick a frozen position: force its confidence below any real one. | |
| conf = conf.masked_fill(~noise_mask, float("-inf")) | |
| k = max(1, min(int(tokens_per_step), conf.shape[-1])) | |
| next_pos = torch.topk(conf, k, dim=-1).indices # [B, k] | |
| # Commit distribution: tempered (temperature == 0 -> greedy argmax). | |
| if temperature and float(temperature) > 0.0: | |
| probs = F.softmax(logits / max(float(temperature), 1e-6), dim=-1) | |
| pred = torch.multinomial( | |
| probs.reshape(-1, vocab_size), num_samples=1, generator=generator | |
| ).view_as(z_t) | |
| else: | |
| pred = logits.argmax(dim=-1) | |
| z_s = z_t.clone() | |
| batch_idx = torch.arange(z_t.shape[0], device=z_t.device).unsqueeze(-1) | |
| z_s[batch_idx, next_pos] = pred[batch_idx, next_pos] | |
| # Restore every frozen position (prompt + anchors) unconditionally. | |
| return torch.where(noise_mask, z_s, z_t), next_pos | |
| def _greedy_step( | |
| z_t: torch.Tensor, | |
| logits: torch.Tensor, | |
| noise_mask: torch.Tensor, | |
| ) -> torch.Tensor: | |
| """One greedy denoising step ``z_t → z_s`` (the original naive Sumi sampler). | |
| Every denoise position is overwritten each step with the model's argmax | |
| prediction; there is no SNR schedule, no temperature and no stochasticity. | |
| Frozen positions (prompt + anchors) are restored via ``noise_mask``. | |
| Shapes: ``z_t [B, S]``, ``logits [B, S, V]``. Returns ``[B, S]``. | |
| """ | |
| pred = logits.argmax(dim=-1) | |
| return torch.where(noise_mask, pred, z_t) | |
| def _build_noise_mask( | |
| generated_ids: torch.Tensor, | |
| prompt_length: int, | |
| frozen: Optional[List] = None, | |
| denoise_end: Optional[List[int]] = None, | |
| ) -> torch.Tensor: | |
| """``[B, S]`` mask (``True`` = denoise here, ``False`` = frozen prompt/anchor). | |
| Prompt positions ``[:prompt_length]`` are frozen. ``frozen`` pins | |
| ``(position, token_id)`` pairs at fixed canvas positions and writes the token id | |
| into ``generated_ids`` in place (a flat ``[(pos, tid), ...]`` shared by every row, | |
| or a PER-ROW ``[[(pos, tid), ...], ...]`` of length ``batch_size``). ``denoise_end`` | |
| (per-row exclusive upper bound) freezes the tail at its prior-random init so the | |
| step budget concentrates on the content window. | |
| """ | |
| batch_size, total_length = generated_ids.shape | |
| device = generated_ids.device | |
| noise_mask = torch.zeros((batch_size, total_length), dtype=torch.bool, device=device) | |
| noise_mask[:, prompt_length:] = True | |
| if frozen: | |
| per_row = len(frozen) > 0 and isinstance(frozen[0], list) | |
| if per_row and len(frozen) != batch_size: | |
| raise ValueError( | |
| f"per-row frozen must have batch_size ({batch_size}) entries, got {len(frozen)}" | |
| ) | |
| for i in range(batch_size): | |
| row_frozen = frozen[i] if per_row else frozen | |
| for pos, tid in row_frozen: | |
| if 0 <= pos < total_length and bool(noise_mask[i, pos]): # not frozen by a prompt | |
| generated_ids[i, pos] = tid | |
| noise_mask[i, pos] = False | |
| if denoise_end is not None: | |
| if len(denoise_end) != batch_size: | |
| raise ValueError( | |
| f"denoise_end must have batch_size ({batch_size}) entries, got {len(denoise_end)}" | |
| ) | |
| for i in range(batch_size): | |
| end = int(denoise_end[i]) | |
| if 0 <= end < total_length: | |
| noise_mask[i, end:] = False # tail stays at its prior-random init | |
| return noise_mask | |
| class SumiGenerationMixin(GenerationMixin): | |
| """Generation mixin for uniform diffusion language models.""" | |
| def _resolve(value, name, default, generation_config, self_generation_config): | |
| """Explicit kwarg → passed generation_config → ``self.generation_config`` → default.""" | |
| if value is not None: | |
| return value | |
| for config in (generation_config, self_generation_config): | |
| if config is not None: | |
| resolved = getattr(config, name, None) | |
| if resolved is not None: | |
| return resolved | |
| return default | |
| def _trim_at_eos(self, generated_ids, prompt_length): | |
| """Cut each row at the first EOS in its generated region (dropping the EOS, the | |
| anchored BOS delimiter, and the denoised tail), then right-pad the batch back into | |
| a rectangular tensor. Prompt tokens are kept. Mirrors the eval harness's | |
| ``_extract_text`` (`gen_ids[:gen_ids.index(eos)]`). The pad filler is a special | |
| token, so ``decode(..., skip_special_tokens=True)`` yields clean text.""" | |
| eos_id = self.config.eos_token_id | |
| if eos_id is None: | |
| return generated_ids | |
| pad_id = self.config.pad_token_id | |
| pad_id = eos_id if pad_id is None else pad_id | |
| batch_size = generated_ids.shape[0] | |
| rows, max_len = [], prompt_length | |
| for i in range(batch_size): | |
| row = generated_ids[i] | |
| hit = (row[prompt_length:] == eos_id).nonzero(as_tuple=True)[0] | |
| cut = prompt_length + int(hit[0].item()) if hit.numel() > 0 else row.shape[0] | |
| rows.append(row[:cut]) | |
| max_len = max(max_len, cut) | |
| out = torch.full((batch_size, max(max_len, 1)), pad_id, | |
| dtype=generated_ids.dtype, device=generated_ids.device) | |
| for i, row in enumerate(rows): | |
| if row.shape[0] > 0: | |
| out[i, : row.shape[0]] = row | |
| return out | |
| def generate( | |
| self, | |
| input_ids: Optional[torch.LongTensor] = None, | |
| attention_mask: Optional[torch.Tensor] = None, | |
| max_new_tokens: Optional[int] = None, | |
| max_length: Optional[int] = None, | |
| num_denoising_steps: Optional[int] = None, | |
| seed: Optional[int] = None, | |
| generator: Optional[torch.Generator] = None, | |
| sampler: Optional[str] = None, | |
| schedule: Optional[str] = None, | |
| min_log_snr: Optional[float] = None, | |
| max_log_snr: Optional[float] = None, | |
| temperature: Optional[float] = None, | |
| tokens_per_step: Optional[int] = None, | |
| frozen: Optional[List] = None, | |
| denoise_end: Optional[List[int]] = None, | |
| canvas_length: Optional[int] = None, | |
| anchor_eosbos: Optional[bool] = None, | |
| trim_at_eos: Optional[bool] = None, | |
| progress_callback: Optional[Callable] = None, | |
| **kwargs, | |
| ) -> SumiGenerationOutput: | |
| generation_config = kwargs.pop("generation_config", None) | |
| inputs = kwargs.pop("inputs", None) | |
| inputs_embeds = kwargs.pop("inputs_embeds", None) | |
| self_generation_config = getattr(self, "generation_config", None) | |
| if inputs_embeds is not None: | |
| raise ValueError("Diffusion generation currently requires input_ids; inputs_embeds is not supported.") | |
| if input_ids is None: | |
| input_ids = inputs | |
| kwargs.pop("return_dict_in_generate", None) | |
| num_denoising_steps = self._resolve( | |
| num_denoising_steps, "num_denoising_steps", 128, generation_config, self_generation_config | |
| ) | |
| if num_denoising_steps < 1: | |
| raise ValueError("num_denoising_steps must be a positive integer.") | |
| sampler = self._resolve(sampler, "sampler", "ancestral", generation_config, self_generation_config) | |
| if sampler not in ("ancestral", "adaptive", "greedy"): | |
| raise ValueError( | |
| f"sampler must be 'ancestral', 'adaptive' or 'greedy', got {sampler!r}" | |
| ) | |
| schedule = self._resolve(schedule, "schedule", "linear", generation_config, self_generation_config) | |
| min_log_snr = self._resolve(min_log_snr, "min_log_snr", -9.0, generation_config, self_generation_config) | |
| max_log_snr = self._resolve(max_log_snr, "max_log_snr", 9.0, generation_config, self_generation_config) | |
| temperature = self._resolve(temperature, "temperature", 1.0, generation_config, self_generation_config) | |
| tokens_per_step = self._resolve( | |
| tokens_per_step, "tokens_per_step", 1, generation_config, self_generation_config | |
| ) | |
| canvas_length = self._resolve(canvas_length, "canvas_length", 2048, generation_config, self_generation_config) | |
| anchor_eosbos = self._resolve(anchor_eosbos, "anchor_eosbos", True, generation_config, self_generation_config) | |
| trim_at_eos = self._resolve(trim_at_eos, "trim_at_eos", True, generation_config, self_generation_config) | |
| device = self.model.embed_tokens.weight.device | |
| if input_ids is None: | |
| bos_token_id = kwargs.pop("bos_token_id", None) | |
| if bos_token_id is None and generation_config is not None: | |
| bos_token_id = getattr(generation_config, "bos_token_id", None) | |
| if bos_token_id is None: | |
| bos_token_id = self.config.bos_token_id | |
| if bos_token_id is None: | |
| raise ValueError("input_ids is required when bos_token_id is not configured.") | |
| input_ids = torch.tensor([[bos_token_id]], dtype=torch.long, device=device) | |
| elif not isinstance(input_ids, torch.Tensor): | |
| input_ids = torch.tensor(input_ids, dtype=torch.long, device=device) | |
| else: | |
| input_ids = input_ids.to(device=device, dtype=torch.long) | |
| if input_ids.dim() == 1: | |
| input_ids = input_ids.unsqueeze(0) | |
| if input_ids.dim() != 2: | |
| raise ValueError("input_ids must have shape (batch_size, sequence_length).") | |
| batch_size, prompt_length = input_ids.shape | |
| if max_new_tokens is None and generation_config is not None: | |
| max_new_tokens = getattr(generation_config, "max_new_tokens", None) | |
| if max_new_tokens is None and self_generation_config is not None: | |
| max_new_tokens = getattr(self_generation_config, "max_new_tokens", None) | |
| if max_new_tokens is None: | |
| if max_length is None and generation_config is not None: | |
| max_length = getattr(generation_config, "max_length", None) | |
| if max_length is None and self_generation_config is not None: | |
| max_length = getattr(self_generation_config, "max_length", None) | |
| if max_length is None: | |
| raise ValueError("Either max_new_tokens or max_length must be provided for diffusion generation.") | |
| max_new_tokens = max_length - prompt_length | |
| if max_new_tokens < 0: | |
| raise ValueError("max_new_tokens must be non-negative.") | |
| if max_new_tokens == 0: | |
| return SumiGenerationOutput(sequences=input_ids, canvas=input_ids) | |
| # The model is trained on a packed, fixed-length canvas, so generation runs on a | |
| # full `canvas_length` canvas (default 2048) rather than just prompt+max_new_tokens. | |
| # `max_new_tokens` is the content budget: the EOS,BOS document delimiter is anchored | |
| # at prompt_length+max_new_tokens, the rest of the canvas is denoised as context, and | |
| # decoding is cut at the first EOS (trim_at_eos, default True). | |
| ceiling = self.config.max_position_embeddings | |
| canvas_length = min(int(canvas_length), ceiling) | |
| reserve = 2 if anchor_eosbos else 0 | |
| if prompt_length + reserve >= canvas_length: | |
| raise ValueError( | |
| f"prompt_length ({prompt_length}) leaves no room in canvas_length ({canvas_length})." | |
| ) | |
| budget = max(1, min(max_new_tokens, canvas_length - prompt_length - reserve)) | |
| total_length = canvas_length | |
| completion_length = total_length - prompt_length | |
| # Anchor the EOS,BOS delimiter at the end of the content budget. The caller can | |
| # override by passing an explicit `frozen` set (then it takes precedence). | |
| if anchor_eosbos and frozen is None: | |
| eos_pos = prompt_length + budget | |
| frozen = [(eos_pos, self.config.eos_token_id), (eos_pos + 1, self.config.bos_token_id)] | |
| if attention_mask is None: | |
| attention_mask = torch.ones_like(input_ids) | |
| elif not isinstance(attention_mask, torch.Tensor): | |
| attention_mask = torch.tensor(attention_mask, device=device) | |
| else: | |
| attention_mask = attention_mask.to(device=device) | |
| if attention_mask.dim() == 1: | |
| attention_mask = attention_mask.unsqueeze(0) | |
| if attention_mask.shape != input_ids.shape: | |
| raise ValueError("attention_mask must have the same shape as input_ids.") | |
| if generator is None and seed is not None: | |
| try: | |
| generator = torch.Generator(device=device) | |
| except RuntimeError: | |
| generator = torch.Generator() | |
| generator.manual_seed(seed) | |
| completion_shape = (batch_size, completion_length) | |
| try: | |
| completion_ids = torch.randint( | |
| low=0, | |
| high=self.vocab_size, | |
| size=completion_shape, | |
| dtype=torch.long, | |
| device=device, | |
| generator=generator, | |
| ) | |
| except RuntimeError: | |
| completion_ids = torch.randint( | |
| low=0, | |
| high=self.vocab_size, | |
| size=completion_shape, | |
| dtype=torch.long, | |
| generator=generator, | |
| ).to(device) | |
| generated_ids = torch.cat([input_ids, completion_ids], dim=-1) | |
| completion_attention_mask = torch.ones( | |
| completion_shape, | |
| dtype=attention_mask.dtype, | |
| device=device, | |
| ) | |
| generation_attention_mask = torch.cat([attention_mask, completion_attention_mask], dim=-1) | |
| # noise_mask follows the reference _init_z: prompt frozen at the front, plus | |
| # optional fixed anchors (frozen) and a per-row active-region bound (denoise_end). | |
| noise_mask = _build_noise_mask(generated_ids, prompt_length, frozen, denoise_end) | |
| log_snrs = _make_log_snr_schedule( | |
| num_denoising_steps, schedule, min_log_snr, max_log_snr, device | |
| ) | |
| was_training = self.training | |
| self.eval() | |
| try: | |
| for step in range(num_denoising_steps): | |
| selected_pos = None | |
| log_snr_t = log_snr_s = None | |
| if sampler == "ancestral": | |
| log_snr_t = float(log_snrs[step]) | |
| log_snr_s = float(log_snrs[step + 1]) | |
| x_hat = _compute_x_hat( | |
| self, generated_ids, generation_attention_mask, self.vocab_size, temperature | |
| ) | |
| if progress_callback is not None: | |
| top1_prob, top1_z = x_hat.max(dim=-1) | |
| else: | |
| top1_prob = top1_z = None | |
| z_new = _ancestral_step( | |
| generated_ids, x_hat, log_snr_t, log_snr_s, self.vocab_size, generator | |
| ) | |
| else: | |
| # adaptive / greedy both read the raw logits; the SNR schedule is unused. | |
| logits = _compute_logits(self, generated_ids, generation_attention_mask, self.vocab_size) | |
| if progress_callback is not None: | |
| top1_prob, top1_z = F.softmax(logits, dim=-1).max(dim=-1) | |
| else: | |
| top1_prob = top1_z = None | |
| if sampler == "adaptive": | |
| # Confidence-based commit (Eq. 9). | |
| z_new, selected_pos = _adaptive_step( | |
| generated_ids, logits, noise_mask, tokens_per_step, temperature, generator | |
| ) | |
| else: # greedy: overwrite every denoise position with the argmax. | |
| z_new = _greedy_step(generated_ids, logits, noise_mask) | |
| generated_ids = torch.where(noise_mask, z_new, generated_ids) | |
| if progress_callback is not None: | |
| progress_callback( | |
| { | |
| "step": step + 1, | |
| "num_steps": num_denoising_steps, | |
| "log_snr_t": log_snr_t, | |
| "log_snr_s": log_snr_s, | |
| "z": generated_ids, | |
| "top1_z": top1_z, | |
| "top1_prob": top1_prob, | |
| "noise_mask": noise_mask, | |
| # Adaptive only: the [B, k] confidence-selected positions | |
| # this step committed (None for ancestral). | |
| "selected_pos": selected_pos, | |
| } | |
| ) | |
| finally: | |
| if was_training: | |
| self.train() | |
| sequences = self._trim_at_eos(generated_ids, prompt_length) if trim_at_eos else generated_ids | |
| return SumiGenerationOutput(sequences=sequences, canvas=generated_ids) | |
| __all__ = [ | |
| "SumiGenerationOutput", | |
| "SumiGenerationConfig", | |
| "SumiGenerationMixin", | |
| ] | |