#!/usr/bin/env python3 # /// script # requires-python = ">=3.10" # dependencies = [ # "torch>=2.4", # "transformers>=4.45", # "datasets>=3.2", # "safetensors>=0.4", # "huggingface_hub>=0.25", # "zstandard", # ] # /// """ kiyo_diffusion.py - transforma o DedeProGames/Kiyo-135M (LM autoregressivo, Llama/SmolLM2) num modelo de linguagem de difusao mascarada (estilo LLaDA / Dream / MetaDiffusion). Receita (adaptacao AR -> difusao do DiffuLLaMA / Dream): 1. mesma arquitetura e mesmos pesos do Kiyo, mas atencao BIDIRECIONAL 2. token novo <|mask|> (vocab 49152 -> 49153), embedding inicializado com a media 3. shift: o hidden da posicao i preve o token i+1 (igual no AR), entao o lm_head pre-treinado continua alinhado com o que o Kiyo ja sabe 4. perda de difusao mascarada (MDLM/LLaDA): t ~ U(eps,1), mascara cada token com prob. t, cross-entropy so nos mascarados com peso 1/t (a "nelbo" do log e limite superior da NLL/token) 5. annealing da atencao: causal -> bidirecional nos primeiros --anneal-steps steps 6. sem timestep embedding: a fracao de [MASK] na entrada ja diz o nivel de ruido 7. geracao: comeca tudo [MASK] e revela os tokens mais confiantes, bloco a bloco (LLaDA) Uso: python kiyo_diffusion.py convert --src DedeProGames/Kiyo-135M --out kiyo-diff-init python kiyo_diffusion.py check --model kiyo-diff-init --src DedeProGames/Kiyo-135M python kiyo_diffusion.py train --out runs/kiyo-diff # 1B tokens; usa todas as GPUs (torchrun automatico) python kiyo_diffusion.py train --out /content/drive/MyDrive/kiyo-diff --tokens 5e8 # Colab (retoma sozinho) python kiyo_diffusion.py generate --model runs/kiyo-diff/final --prompt "The cat sat on the" --watch python kiyo_diffusion.py generate --model runs/kiyo-diff/final --gen-len 0 \\ --prompt "The capital of France is<|mask|><|mask|>. It is famous for the Eiffel Tower." # infilling python kiyo_diffusion.py score --model runs/kiyo-diff/final --context "The capital of France is" \\ --choices " Paris" " London" " Rome" """ import argparse import array import glob import json import math import os import platform import random import shutil import sys import time from contextlib import nullcontext os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "60") # o padrao (10 s) da timeout no Colab os.environ.setdefault("HF_HUB_ETAG_TIMEOUT", "60") import torch import torch.nn as nn import torch.nn.functional as F import torch.utils.checkpoint from safetensors.torch import load_file, save_file MASK_TOKEN = "<|mask|>" KIYO_MIX = [ # mesmas fontes do Kiyo; o peso e a fracao de TOKENS (ajusta pro mix que tu usou) "HuggingFaceFW/fineweb-edu@0.45", "mlfoundations/dclm-baseline-1.0@0.35", "HuggingFaceTB/finemath:finemath-3plus@0.10", "HuggingFaceCode/stack-v3-train@0.10", ] # ============================================================================ modelo class RMSNorm(nn.Module): def __init__(self, dim, eps): super().__init__() self.weight = nn.Parameter(torch.ones(dim)) self.eps = eps def forward(self, x): dt = x.dtype x = x.float() x = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) return self.weight * x.to(dt) def rotate_half(x): x1, x2 = x.chunk(2, dim=-1) return torch.cat((-x2, x1), dim=-1) class Attention(nn.Module): def __init__(self, c): super().__init__() self.nh, self.nkv, self.hd = c["num_attention_heads"], c["num_key_value_heads"], c["head_dim"] H = c["hidden_size"] self.q_proj = nn.Linear(H, self.nh * self.hd, bias=False) self.k_proj = nn.Linear(H, self.nkv * self.hd, bias=False) self.v_proj = nn.Linear(H, self.nkv * self.hd, bias=False) self.o_proj = nn.Linear(self.nh * self.hd, H, bias=False) def forward(self, x, cos, sin, attn_mask=None, causal=False): B, L, _ = x.shape q = self.q_proj(x).view(B, L, self.nh, self.hd).transpose(1, 2) k = self.k_proj(x).view(B, L, self.nkv, self.hd).transpose(1, 2) v = self.v_proj(x).view(B, L, self.nkv, self.hd).transpose(1, 2) cos, sin = cos.to(q.dtype), sin.to(q.dtype) q = q * cos + rotate_half(q) * sin k = k * cos + rotate_half(k) * sin if self.nkv != self.nh: # GQA r = self.nh // self.nkv k, v = k.repeat_interleave(r, dim=1), v.repeat_interleave(r, dim=1) # attn_mask=None e causal=False -> atencao bidirecional completa (kernel flash do SDPA) o = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, is_causal=causal and attn_mask is None) return self.o_proj(o.transpose(1, 2).reshape(B, L, -1)) class MLP(nn.Module): def __init__(self, H, I): super().__init__() self.gate_proj = nn.Linear(H, I, bias=False) self.up_proj = nn.Linear(H, I, bias=False) self.down_proj = nn.Linear(I, H, bias=False) def forward(self, x): return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) class Block(nn.Module): def __init__(self, c): super().__init__() self.input_layernorm = RMSNorm(c["hidden_size"], c["rms_norm_eps"]) self.self_attn = Attention(c) self.post_attention_layernorm = RMSNorm(c["hidden_size"], c["rms_norm_eps"]) self.mlp = MLP(c["hidden_size"], c["intermediate_size"]) def forward(self, x, cos, sin, attn_mask=None, causal=False): x = x + self.self_attn(self.input_layernorm(x), cos, sin, attn_mask, causal) return x + self.mlp(self.post_attention_layernorm(x)) class KiyoDiffusion(nn.Module): """Llama identico ao Kiyo (mesmos nomes de pesos), so que sem mascara causal.""" def __init__(self, c): super().__init__() self.cfg = c self.embed_tokens = nn.Embedding(c["vocab_size"], c["hidden_size"]) self.layers = nn.ModuleList([Block(c) for _ in range(c["num_hidden_layers"])]) self.norm = RMSNorm(c["hidden_size"], c["rms_norm_eps"]) tied = c.get("tie_word_embeddings", True) self.lm_head = None if tied else nn.Linear(c["hidden_size"], c["vocab_size"], bias=False) self.grad_ckpt = False def rope(self, L, device): # sempre em fp32 (barato) -> o modelo em bf16 nao perde precisao no RoPE hd = self.cfg["head_dim"] inv = 1.0 / (self.cfg["rope_theta"] ** (torch.arange(0, hd, 2, device=device, dtype=torch.float32) / hd)) f = torch.outer(torch.arange(L, device=device, dtype=torch.float32), inv) emb = torch.cat((f, f), dim=-1) return emb.cos()[None, None], emb.sin()[None, None] def hidden(self, ids, attn_mask=None, causal=False): x = self.embed_tokens(ids) cos, sin = self.rope(ids.shape[1], ids.device) for blk in self.layers: if self.grad_ckpt and self.training: x = torch.utils.checkpoint.checkpoint(blk, x, cos, sin, attn_mask, causal, use_reentrant=False) else: x = blk(x, cos, sin, attn_mask, causal) return self.norm(x) def forward(self, ids, targets=None, loss_mask=None, p_mask=None, attn_mask=None, causal=False): """Sem targets: logits alinhados (logits[:, j] = distribuicao do token da posicao j). Com targets: (nelbo, soma da CE nos mascarados, n de mascarados).""" h = self.hidden(ids, attn_mask, causal) W = self.embed_tokens.weight if self.lm_head is None else self.lm_head.weight shift = self.cfg.get("shift_logits", True) if targets is None: logits = F.linear(h, W) if shift: logits = torch.cat([logits[:, :1], logits[:, :-1]], dim=1) return logits if shift: # hidden i preve o token i+1 h, targets, loss_mask, p_mask = h[:, :-1], targets[:, 1:], loss_mask[:, 1:], p_mask[:, 1:] # lm_head so nas posicoes mascaradas (economiza muita memoria com vocab de 49k) logits = F.linear(h[loss_mask], W).float() logits[:, self.cfg["mask_token_id"]] = -float("inf") # [MASK] nunca e uma resposta valida ce = F.cross_entropy(logits, targets[loss_mask], reduction="none") nelbo = (ce / p_mask[loss_mask]).sum() / targets.numel() return nelbo, ce.detach().sum(), loss_mask.sum() # ============================================================================ IO def read_safetensors(path): idx = os.path.join(path, "model.safetensors.index.json") files = sorted(set(json.load(open(idx))["weight_map"].values())) if os.path.exists(idx) else ["model.safetensors"] sd = {} for f in files: sd.update(load_file(os.path.join(path, f))) return sd def local_dir(path): if os.path.isdir(path): return path from huggingface_hub import snapshot_download return snapshot_download(path, allow_patterns=["*.json", "*.safetensors", "*.txt", "*.py", "*.jinja"]) def write_card(out, cfg, repo=None): base = cfg.get("base_model", "DedeProGames/Kiyo-135M") name = (repo or os.path.basename(os.path.normpath(out))).split("/")[-1] with open(os.path.join(out, "README.md"), "w") as f: f.write(f"""--- license: apache-2.0 language: [en] pipeline_tag: text-generation base_model: {base} tags: [diffusion-language-model, masked-diffusion, autoregressive-conversion, llama, kiyo] --- # {name} Masked diffusion language model adapted from [{base}](https://huggingface.co/{base}): same Llama weights, bidirectional attention, a new `<|mask|>` token, the masked-diffusion (MDLM/LLaDA) objective and the AR shift operation (DiffuLLaMA/Dream recipe). No timestep embedding. Research use only. ```bash python kiyo_diffusion.py generate --model {repo or name} --prompt "The capital of France is" ``` """) def save_pretrained(model, cfg, tok, out, dtype=torch.bfloat16): os.makedirs(out, exist_ok=True) sd = {(k if k.startswith("lm_head") else "model." + k): v.detach().to("cpu", dtype).contiguous() for k, v in model.state_dict().items()} save_file(sd, os.path.join(out, "model.safetensors"), metadata={"format": "pt"}) with open(os.path.join(out, "config.json"), "w") as f: json.dump(cfg, f, indent=2) tok.save_pretrained(out) try: # deixa o repo autocontido (igual o inference.py do MetaDiffusion) shutil.copy(os.path.abspath(__file__), os.path.join(out, "kiyo_diffusion.py")) except (shutil.SameFileError, OSError): pass def load_model(path, device="cpu", dtype=torch.float32): from transformers import AutoTokenizer from transformers.utils import logging as hf_logging hf_logging.set_verbosity_error() # silencia o aviso de model_type desconhecido ao ler o tokenizer path = local_dir(path) cfg = json.load(open(os.path.join(path, "config.json"))) model = KiyoDiffusion(cfg) sd = {k.removeprefix("model."): v for k, v in read_safetensors(path).items()} model.load_state_dict(sd, strict=True) return model.to(device=device, dtype=dtype), cfg, AutoTokenizer.from_pretrained(path) # ============================================================================ convert def convert(src_id, out, shift=True): from transformers import AutoTokenizer src = local_dir(src_id) hc = json.load(open(os.path.join(src, "config.json"))) assert hc.get("model_type", "llama") == "llama", "so checkpoints Llama sao suportados" assert hc.get("rope_scaling") is None, "rope_scaling nao suportado neste script" rope_theta = hc.get("rope_theta") or (hc.get("rope_parameters") or {}).get("rope_theta", 10000.0) tok = AutoTokenizer.from_pretrained(src) tok.add_special_tokens({"mask_token": MASK_TOKEN}) mask_id = tok.convert_tokens_to_ids(MASK_TOKEN) tied = hc.get("tie_word_embeddings", True) sd = {k.removeprefix("model."): v.float() for k, v in read_safetensors(src).items()} if tied: sd.pop("lm_head.weight", None) V_old, H = sd["embed_tokens.weight"].shape V = max(V_old, mask_id + 1) for key in ["embed_tokens.weight"] + ([] if tied else ["lm_head.weight"]): E = sd[key] newE = torch.empty(V, H) newE[:V_old] = E newE[V_old:] = E.mean(0) # [MASK] comeca como o embedding medio sd[key] = newE cfg = {k: hc[k] for k in ("hidden_size", "intermediate_size", "num_hidden_layers", "num_attention_heads", "num_key_value_heads", "rms_norm_eps", "max_position_embeddings")} cfg["rope_theta"] = float(rope_theta) cfg["head_dim"] = hc.get("head_dim") or cfg["hidden_size"] // cfg["num_attention_heads"] cfg.update(architectures=["KiyoDiffusion"], model_type="kiyo_diffusion", vocab_size=V, mask_token_id=mask_id, eos_token_id=tok.eos_token_id, bos_token_id=tok.bos_token_id if tok.bos_token_id is not None else tok.eos_token_id, tie_word_embeddings=tied, shift_logits=shift, base_model=src_id) model = KiyoDiffusion(cfg) model.load_state_dict(sd, strict=True) save_pretrained(model, cfg, tok, out) n = sum(p.numel() for p in model.parameters()) print(f"ok -> {out} | {n / 1e6:.2f}M params | vocab {V} | mask_id {mask_id} | " f"rope_theta {rope_theta} | shift {shift}", flush=True) def cmd_convert(a): convert(a.src, a.out, shift=not a.no_shift) # ============================================================================ check CHECK_TEXT = ("Photosynthesis is the process by which green plants, algae and some bacteria convert light " "energy into chemical energy. During this process, carbon dioxide and water are combined to " "form glucose, and oxygen is released as a by-product. The reactions take place inside the " "chloroplasts, where the pigment chlorophyll absorbs mostly blue and red light. Because almost " "every food chain on Earth begins with organisms that photosynthesize, the process is essential " "for life as we know it.") @torch.no_grad() def cmd_check(a): """Modo causal do modelo convertido tem que bater com o Kiyo original (valida pesos/RoPE).""" from transformers import AutoModelForCausalLM model, cfg, tok = load_model(a.model) model.eval() ref = AutoModelForCausalLM.from_pretrained(local_dir(a.src), dtype=torch.float32).eval() ids = torch.tensor([tok(a.text or CHECK_TEXT, add_special_tokens=False).input_ids[:512]]) Vref = ref.config.vocab_size ours = model(ids, causal=True) ours = (ours[:, 1:] if cfg.get("shift_logits", True) else ours[:, :-1])[..., :Vref] theirs = ref(ids).logits[:, :-1] print(f"max |dif logits| vs Kiyo original: {(ours - theirs).abs().max().item():.2e} (esperado < 1e-3)") print(f"loss AR do Kiyo neste texto: {F.cross_entropy(theirs[0], ids[0, 1:]).item():.3f}") torch.manual_seed(0) for r in (0.15, 0.5, 0.9): # CE media nos tokens mascarados, atencao bidirecional, antes de treinar m = torch.rand(ids.shape) < r m[:, 0] = False logits = model(torch.where(m, cfg["mask_token_id"], ids)) print(f"difusao (sem treino) | {int(r * 100)}% mascarado -> CE {F.cross_entropy(logits[m], ids[m]).item():.3f}") # ============================================================================ dados def _texts(ex): if ex.get("text"): return [ex["text"]] if ex.get("content"): return [ex["content"]] if ex.get("files"): # The Stack v3: cada linha e um repositorio inteiro return [f["content"] for f in ex["files"] if f.get("content") and not f.get("is_vendor")] return [] class PackedStream(torch.utils.data.IterableDataset): """Streaming do HF -> tokeniza -> empacota em seqs de seq_len (docs separados por EOS). Fontes: repo[:config][@peso] (ex: HuggingFaceTB/finemath:finemath-3plus@0.1) ou arquivo local .txt/.jsonl/.parquet (aceita glob). O peso e a fracao de TOKENS de cada fonte. Os arquivos de cada fonte sao divididos entre GPUs x workers, entao ninguem baixa dado repetido. O shuffle e em nivel de DOCUMENTO (pool de textos ja extraidos): um buffer de linhas estoura a RAM na Stack v3, onde cada linha e um repositorio inteiro com metadados.""" def __init__(self, specs, tok_path, seq_len, seed, rank, world, pool_size=2000): super().__init__() self.specs, self.tok_path, self.seq_len = specs, tok_path, seq_len self.seed, self.rank, self.world, self.pool_size = seed, rank, world, pool_size self._file_cache = {} def _files(self, name): """Lista de arquivos de uma fonte: glob local, ou os data_files (hf://) do dataset no Hub.""" if name not in self._file_cache: if any(x in name.lower() for x in (".txt", ".json", ".parquet")): files = sorted(glob.glob(name)) or [name] else: from datasets import load_dataset_builder repo, _, conf = name.partition(":") files = sorted(load_dataset_builder(repo, conf or None).config.data_files["train"]) self._file_cache[name] = files return self._file_cache[name] @staticmethod def _rows(path): if path.endswith(".parquet"): # pyarrow direto, em lotes pequenos: o datasets descomprime row groups inteiros # (ate 360 MB na Stack v3) e estoura a RAM import fsspec import pyarrow.parquet as pq with fsspec.open(path, "rb", block_size=8 << 20) as fh: pf = pq.ParquetFile(fh, pre_buffer=False, buffer_size=8 << 20) cols = [c for c in ("text", "content", "files") if c in pf.schema_arrow.names] for batch in pf.iter_batches(batch_size=32, columns=cols, use_threads=False): yield from batch.to_pylist() else: # .jsonl(.zst/.gz) e .txt (DCLM, arquivos locais), lidos direto linha a linha. # Nao usar o datasets aqui: dentro de um worker do DataLoader ele "desliga" os workers # extras quando o arquivo tem 1 shard, e o worker fica varrendo arquivos vazios. import fsspec with fsspec.open(path, "rt", compression="infer", encoding="utf-8", errors="ignore", block_size=8 << 20) as fh: for line in fh: if ".txt" in path: yield {"text": line} elif line.strip(): try: yield json.loads(line) except json.JSONDecodeError: continue def _open(self, name, shard, n_shards, epoch): files = list(self._files(name)) random.Random(self.seed + epoch).shuffle(files) # ordem aleatoria de arquivos a cada epoca if len(files) >= n_shards: # arquivos inteiros por worker: ninguem baixa dado repetido for path in files[shard::n_shards]: yield from self._rows(path) else: # poucos arquivos (ex: 1 arquivo local): 1 a cada n linhas k = 0 for path in files: for row in self._rows(path): if k % n_shards == shard: yield row k += 1 def __iter__(self): from transformers import AutoTokenizer from transformers.utils import logging as hf_logging hf_logging.set_verbosity_error() tok = AutoTokenizer.from_pretrained(self.tok_path) eos = tok.eos_token_id wi = torch.utils.data.get_worker_info() nw, wid = (wi.num_workers, wi.id) if wi else (1, 0) shard, n_shards = self.rank * nw + wid, self.world * nw srcs = [(s.partition("@")[0], float(s.partition("@")[2] or 1.0)) for s in self.specs] its = [self._open(name, shard, n_shards, 0) for name, _ in srcs] epoch, rows = [0] * len(srcs), [0] * len(srcs) bufs = [array.array("i") for _ in srcs] pools = [[] for _ in srcs] sent = [0] * len(srcs) rng = random.Random(self.seed * 1000 + shard) def next_doc(i): pool, empty = pools[i], 0 while len(pool) < self.pool_size: # reabastece o pool de documentos da fonte i try: ex = next(its[i]) except StopIteration: # fonte acabou: reembaralha e recomeca if rows[i] == 0: raise RuntimeError(f"fonte vazia neste shard: {srcs[i][0]}") epoch[i], rows[i] = epoch[i] + 1, 0 its[i] = self._open(srcs[i][0], shard, n_shards, epoch[i]) continue rows[i] += 1 got = _texts(ex) empty = 0 if got else empty + 1 if empty > 1000: raise RuntimeError(f"{srcs[i][0]}: sem coluna 'text', 'content' ou 'files'") pool.extend(got) j = rng.randrange(len(pool)) # tira um documento aleatorio do pool pool[j], pool[-1] = pool[-1], pool[j] return pool.pop() while True: # escolhe a fonte mais atrasada em relacao ao peso -> mix por tokens, nao por documentos i = min(range(len(srcs)), key=lambda j: sent[j] / srcs[j][1]) while len(bufs[i]) < self.seq_len: docs = [next_doc(i) for _ in range(16)] # split_special_tokens: um "<|mask|>" ou "<|endoftext|>" literal no texto vira texto normal for ids in tok(docs, add_special_tokens=False, split_special_tokens=True)["input_ids"]: bufs[i].extend(ids) bufs[i].append(eos) block = torch.frombuffer(bufs[i][:self.seq_len], dtype=torch.int32).long() del bufs[i][:self.seq_len] sent[i] += 1 yield block # ============================================================================ treino def noise(x0, mask_id, eps=1e-3): B, L = x0.shape # um t por sequencia, estratificado no batch (menos variancia que t sorteado independente) u = (torch.rand(1, device=x0.device) + torch.arange(B, device=x0.device) / B) % 1.0 p = ((1 - eps) * u + eps)[:, None].expand(B, L) m = torch.rand(B, L, device=x0.device) < p m[:, 0] = False # ancora: o shift precisa de um 1o token sempre visivel return torch.where(m, mask_id, x0), m, p def anneal_mask(L, frac, device): """frac=0 -> causal puro; frac=1 -> bidirecional (None = kernel flash sem mascara).""" if frac >= 1.0: return None causal = torch.ones(L, L, dtype=torch.bool, device=device).tril() return causal | (torch.rand(L, L, device=device) < frac) def lr_at(step, a): if step < a.warmup: return a.lr * (step + 1) / a.warmup prog = min(1.0, (step - a.warmup) / max(1, a.steps - a.warmup)) return a.min_lr + 0.5 * (a.lr - a.min_lr) * (1 + math.cos(math.pi * prog)) def pick_dtype(name="auto"): """bf16 nas GPUs Ampere+ (3060, L4, A100...), fp16 nas mais antigas (T4, sem bf16), fp32 na CPU.""" if name != "auto": return {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[name] if not torch.cuda.is_available(): return torch.float32 return torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float16 def maybe_relaunch(a): """Se tem varias GPUs e nao veio do torchrun, relanca o proprio script com torchrun.""" if "LOCAL_RANK" in os.environ or not torch.cuda.is_available(): return n = torch.cuda.device_count() if a.nproc <= 0 else a.nproc if n > 1: print(f"relancando com torchrun em {n} GPUs", flush=True) os.execv(sys.executable, [sys.executable, "-m", "torch.distributed.run", "--standalone", f"--nproc_per_node={n}", os.path.abspath(__file__), *sys.argv[1:]]) def resolve_init(a, master, ddp): """--init pode ser o Kiyo AR (converte na hora pra /init) ou um modelo de difusao ja salvo.""" target = os.path.join(a.out, "init") def is_diffusion(): with open(os.path.join(local_dir(a.init), "config.json")) as f: return json.load(f).get("model_type") == "kiyo_diffusion" if master and not is_diffusion() and not os.path.exists(os.path.join(target, "model.safetensors")): convert(a.init, target, shift=not a.no_shift) if ddp: torch.distributed.barrier() return local_dir(a.init) if is_diffusion() else target def cmd_train(a): maybe_relaunch(a) ddp = int(os.environ.get("WORLD_SIZE", "1")) > 1 cuda = torch.cuda.is_available() if ddp: import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP if cuda and "microsoft" in platform.uname().release.lower(): os.environ.setdefault("NCCL_P2P_DISABLE", "1") # NCCL no WSL2 costuma travar com P2P ligado dist.init_process_group("nccl" if cuda else "gloo") rank, world, local = dist.get_rank(), dist.get_world_size(), int(os.environ["LOCAL_RANK"]) device = torch.device("cuda", local) if cuda else torch.device("cpu") if cuda: torch.cuda.set_device(local) else: rank, world, local = 0, 1, 0 device = torch.device("cuda" if cuda else "cpu") master = rank == 0 log = (lambda *x: print(*x, flush=True)) if master else (lambda *x: None) torch.manual_seed(a.seed + rank) random.seed(a.seed + rank) torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True init = resolve_init(a, master, ddp) model, cfg, tok = load_model(init, device, torch.float32) model.grad_ckpt = a.grad_ckpt mask_id = cfg["mask_token_id"] if a.seq_len > cfg["max_position_embeddings"]: log(f"aviso: --seq-len {a.seq_len} > max_position_embeddings {cfg['max_position_embeddings']}") amp_dtype = pick_dtype(a.dtype) if cuda else torch.float32 amp = (lambda: torch.autocast("cuda", dtype=amp_dtype)) if amp_dtype != torch.float32 else nullcontext scaler = torch.amp.GradScaler("cuda", enabled=cuda and amp_dtype == torch.float16) # so fp16 (T4) params = list(model.parameters()) opt = torch.optim.AdamW([{"params": [p for p in params if p.dim() >= 2], "weight_decay": a.wd}, {"params": [p for p in params if p.dim() < 2], "weight_decay": 0.0}], lr=a.lr, betas=(0.9, 0.95), eps=1e-8, fused=cuda) step, ck_dir = 0, os.path.join(a.out, "checkpoints") cks = sorted(glob.glob(os.path.join(ck_dir, "step_*.pt"))) if cks: s = torch.load(cks[-1], map_location=device, weights_only=False) model.load_state_dict(s["model"]) opt.load_state_dict(s["opt"]) if scaler.is_enabled() and "scaler" in s: scaler.load_state_dict(s["scaler"]) step = s["step"] del s log(f"retomando de {cks[-1]} (step {step})") if a.compile: for blk in model.layers: # compila bloco a bloco, in-place (nomes do state_dict nao mudam) blk.compile() ddp_model = DDP(model, device_ids=[local] if cuda else None) if ddp else None fwd = ddp_model or model per_micro = a.micro_bs * a.seq_len * world accum = max(1, round(a.batch_tokens / per_micro)) tok_step = per_micro * accum a.steps = a.steps or math.ceil(a.tokens / tok_step) # mesmo batch global em qualquer maquina anneal_steps = int(a.anneal_frac * a.steps) log(f"{sum(p.numel() for p in params) / 1e6:.2f}M params | rope_theta {cfg['rope_theta']} | " f"shift {cfg.get('shift_logits', True)} | {str(amp_dtype).removeprefix('torch.')}\n" f"{a.micro_bs} seq x {a.seq_len} tok x {world} GPU x {accum} accum = {tok_step:,} tokens/step | " f"{a.steps} steps = {tok_step * a.steps / 1e9:.2f}B tokens | annealing {anneal_steps} steps") def make_iter(seed): ds = PackedStream(a.data, init, a.seq_len, seed, rank, world) return iter(torch.utils.data.DataLoader(ds, batch_size=a.micro_bs, num_workers=a.workers, pin_memory=cuda, prefetch_factor=4 if a.workers > 0 else None)) data_seed = a.seed + step # ordem de dados diferente a cada retomada it = make_iter(data_seed) def next_batch(): nonlocal it, data_seed for tries in range(10): try: return next(it) except Exception as e: # soluco de rede no streaming: espera e reabre print(f"[rank {rank}] erro nos dados: {e!r} -> reabrindo o stream", flush=True) time.sleep(min(60, 5 * (tries + 1))) data_seed += 1000 it = make_iter(data_seed) raise RuntimeError("o stream de dados falhou 10 vezes seguidas") model.train() stats = torch.zeros(4, device=device) # soma nelbo, n micro-batches, soma CE, n mascarados t0 = t_start = last_save = time.time() step0 = last_log = step while step < a.steps: lr = lr_at(step, a) for g in opt.param_groups: g["lr"] = lr frac = 1.0 if anneal_steps <= 0 else min(1.0, step / anneal_steps) for micro in range(accum): x0 = next_batch().to(device, non_blocking=True) if random.random() < a.var_len_prob: # as vezes seqs curtas (LLaDA usa 1%) x0 = x0[:, :random.randint(16, a.seq_len)] xt, m, p = noise(x0, mask_id, a.eps) am = anneal_mask(x0.shape[1], frac, device) if ddp_model is not None: # so sincroniza gradiente no ultimo micro-batch ddp_model.require_backward_grad_sync = micro == accum - 1 with amp(): nelbo, ce_sum, n_masked = fwd(xt, targets=x0, loss_mask=m, p_mask=p, attn_mask=am) scaler.scale(nelbo / accum).backward() stats += torch.stack([nelbo.detach().float(), torch.ones((), device=device), ce_sum.float(), n_masked.float()]) scaler.unscale_(opt) gn = torch.nn.utils.clip_grad_norm_(params, a.clip) scaler.step(opt) scaler.update() opt.zero_grad(set_to_none=True) step += 1 if step % a.log_every == 0 or step == a.steps: if ddp: dist.all_reduce(stats) dt = time.time() - t0 eta = (time.time() - t_start) / (step - step0) * (a.steps - step) log(f"step {step:>7} | nelbo {stats[0] / stats[1]:.4f} | ce_mask {stats[2] / stats[3].clamp(min=1):.4f} | " f"lr {lr:.2e} | gnorm {float(gn):.2f} | attn_bidir {frac:.2f} | " f"{(step - last_log) * tok_step / dt:,.0f} tok/s | eta {eta / 3600:.1f}h") stats.zero_() t0, last_log = time.time(), step if master and a.sample_every and step % a.sample_every == 0: model.eval() with amp(): txt = generate(model, tok, a.sample_prompt, gen_len=48, block_len=16, temperature=0.0, rep_penalty=1.1) log(f"[amostra step {step}] {txt!r}") model.train() want = step % a.save_every == 0 or step == a.steps or \ (a.save_minutes > 0 and time.time() - last_save > a.save_minutes * 60) if ddp: flag = torch.tensor([int(want)], device=device) dist.broadcast(flag, src=0) want = bool(flag.item()) if want: last_save = time.time() if master: os.makedirs(ck_dir, exist_ok=True) path = os.path.join(ck_dir, f"step_{step:08d}.pt") torch.save({"model": model.state_dict(), "opt": opt.state_dict(), "step": step, "scaler": scaler.state_dict(), "args": vars(a)}, path + ".tmp") os.replace(path + ".tmp", path) for old in sorted(glob.glob(os.path.join(ck_dir, "step_*.pt")))[:-a.keep_last]: os.remove(old) done = step == a.steps out = os.path.join(a.out, "final" if done else "latest") save_pretrained(model, cfg, tok, out) if done: write_card(out, cfg, a.push_to) log(f"checkpoint salvo: {path}") if ddp: dist.barrier() if master and a.push_to: from huggingface_hub import HfApi api = HfApi() api.create_repo(a.push_to, private=not a.public, exist_ok=True) api.upload_folder(folder_path=os.path.join(a.out, "final"), repo_id=a.push_to) log(f"enviado para https://huggingface.co/{a.push_to}") if ddp: dist.barrier() dist.destroy_process_group() # ============================================================================ geracao @torch.no_grad() def generate_ids(model, ids, gen_len=128, block_len=32, steps=None, temperature=0.6, rep_penalty=1.0, stop_at_eos=True, on_step=None): """Sampler do LLaDA: blocos da esquerda p/ direita + revela os tokens mais confiantes. Qualquer [MASK] dentro de `ids` tambem e preenchido (infilling).""" c = model.cfg mask_id, eos, dev = c["mask_token_id"], c["eos_token_id"], next(model.parameters()).device x = torch.tensor([list(ids) + [mask_id] * gen_len], dtype=torch.long, device=dev) P = len(ids) todo = (x[0] == mask_id).nonzero().squeeze(1) if todo.numel() == 0: return x[0] blocks = todo.split(block_len) spb = max(1, math.ceil((steps or todo.numel()) / len(blocks))) # steps=None -> 1 token por step for b, blk in enumerate(blocks): for i in range(spb): left = blk[x[0, blk] == mask_id] if left.numel() == 0: break k = math.ceil(left.numel() / (spb - i)) logits = model(x)[0, left].float() logits[:, mask_id] = -float("inf") if rep_penalty != 1.0: seen = x[0][x[0] != mask_id].unique() sl = logits[:, seen] logits[:, seen] = torch.where(sl > 0, sl / rep_penalty, sl * rep_penalty) if temperature > 0: probs = F.softmax(logits / temperature, dim=-1) pred = torch.multinomial(probs, 1).squeeze(-1) else: probs = F.softmax(logits, dim=-1) pred = probs.argmax(-1) conf = probs.gather(-1, pred[:, None]).squeeze(-1) pick = conf.topk(k).indices # revela so os k mais confiantes, o resto continua [MASK] x[0, left[pick]] = pred[pick] if on_step: on_step(x[0], b, len(blocks), i, spb) if stop_at_eos and gen_len > 0 and (x[0, P:] == eos).any(): break return x[0] def generate(model, tok, prompt, gen_len=128, block_len=32, steps=None, temperature=0.6, rep_penalty=1.0, watch=False, bos=True): c = model.cfg ids = tok(prompt, add_special_tokens=False).input_ids # "<|mask|>" no prompt vira [MASK] -> infilling start = 0 if bos or not ids: # o shift precisa de >= 1 token visivel antes da 1a mascara ids, start = [c.get("bos_token_id", c["eos_token_id"])] + ids, 1 P = len(ids) def show(x): return tok.decode(x[start:].tolist()).replace(MASK_TOKEN, "_") def on_step(x, b, nb, i, spb): print(f"[bloco {b + 1}/{nb} step {i + 1}/{spb}] {show(x)}\n", flush=True) out = generate_ids(model, ids, gen_len, block_len, steps, temperature, rep_penalty, on_step=on_step if watch else None).tolist() end = len(out) if gen_len > 0 and c["eos_token_id"] in out[P:]: end = P + out[P:].index(c["eos_token_id"]) return tok.decode(out[start:end]).replace(MASK_TOKEN, "_") @torch.no_grad() def score(model, tok, context, continuation, n_mc=32): """Log-verossimilhanca aproximada (ELBO por Monte Carlo, como no LLaDA) de `continuation` dado `context`. Serve p/ benchmarks de multipla escolha: maior = alternativa mais provavel.""" c = model.cfg mask_id, dev = c["mask_token_id"], next(model.parameters()).device ctx = [c.get("bos_token_id", c["eos_token_id"])] + tok(context, add_special_tokens=False).input_ids cont = tok(continuation, add_special_tokens=False).input_ids P, Lc = len(ctx), len(cont) x0 = torch.tensor([ctx + cont], device=dev).repeat(n_mc, 1) l = torch.randint(1, Lc + 1, (n_mc,), device=dev) # quantos tokens da resposta mascarar m = torch.zeros_like(x0, dtype=torch.bool) m[:, P:] = torch.rand(n_mc, Lc, device=dev).argsort(-1) < l[:, None] logp = torch.log_softmax(model(torch.where(m, mask_id, x0)).float(), -1) logp = logp.gather(-1, x0[..., None]).squeeze(-1) return ((logp * m).sum(-1) * (Lc / l.float())).mean().item() def _load_for_inference(path): cuda = torch.cuda.is_available() model, cfg, tok = load_model(path, "cuda" if cuda else "cpu", pick_dtype()) return model.eval(), tok def cmd_generate(a): model, tok = _load_for_inference(a.model) torch.manual_seed(a.seed) print(generate(model, tok, a.prompt, a.gen_len, a.block_len, a.steps, a.temperature, a.rep_penalty, a.watch, bos=not a.no_bos)) def cmd_score(a): model, tok = _load_for_inference(a.model) torch.manual_seed(a.seed) res = [(score(model, tok, a.context, ch, a.n_mc), ch) for ch in a.choices] for s, ch in sorted(res, reverse=True): print(f"{s:10.3f} {ch!r}") # ============================================================================ CLI def main(): ap = argparse.ArgumentParser(description="Kiyo -> LM de difusao mascarada") sub = ap.add_subparsers(dest="cmd", required=True) c = sub.add_parser("convert", help="converte o checkpoint AR (o train ja faz isso sozinho)") c.add_argument("--src", default="DedeProGames/Kiyo-135M") c.add_argument("--out", default="kiyo-diff-init") c.add_argument("--no-shift", action="store_true", help="sem shift (estilo MetaDiffusion/LLaDA)") k = sub.add_parser("check", help="compara o modo causal com o Kiyo original") k.add_argument("--model", default="kiyo-diff-init") k.add_argument("--src", default="DedeProGames/Kiyo-135M") k.add_argument("--text", default=None) t = sub.add_parser("train") t.add_argument("--init", default="DedeProGames/Kiyo-135M", help="checkpoint AR (converte sozinho) ou modelo de difusao ja salvo") t.add_argument("--out", default="runs/kiyo-diff") t.add_argument("--data", nargs="+", default=KIYO_MIX, help="repo[:config][@peso] ou arquivos locais") t.add_argument("--seq-len", type=int, default=1024) t.add_argument("--micro-bs", type=int, default=4, help="sequencias por GPU por micro-batch (OOM? baixa isso ou usa --grad-ckpt)") t.add_argument("--batch-tokens", type=int, default=131_072, help="tokens por step, global (o accum se ajusta ao n de GPUs)") t.add_argument("--tokens", type=float, default=1e9, help="orcamento total (1e9 = 1B, 5e8 = 500M)") t.add_argument("--steps", type=int, default=None, help="sobrescreve --tokens") t.add_argument("--lr", type=float, default=3e-4) t.add_argument("--min-lr", type=float, default=3e-5) t.add_argument("--warmup", type=int, default=200) t.add_argument("--wd", type=float, default=0.1) t.add_argument("--clip", type=float, default=1.0) t.add_argument("--eps", type=float, default=1e-3, help="taxa minima de mascaramento") t.add_argument("--anneal-frac", type=float, default=0.2, help="fracao dos steps com annealing causal->bidirecional (0 = bidirecional direto)") t.add_argument("--no-shift", action="store_true", help="so vale na conversao automatica") t.add_argument("--var-len-prob", type=float, default=0.01) t.add_argument("--workers", type=int, default=2, help="workers do dataloader por GPU") t.add_argument("--nproc", type=int, default=0, help="n de GPUs (0 = todas)") t.add_argument("--log-every", type=int, default=10) t.add_argument("--save-every", type=int, default=250) t.add_argument("--save-minutes", type=float, default=30, help="salva tambem a cada N min (0 = off)") t.add_argument("--keep-last", type=int, default=3, help="cada checkpoint .pt tem ~1,6 GB") t.add_argument("--sample-every", type=int, default=250, help="0 = sem amostras no log") t.add_argument("--dtype", default="auto", choices=["auto", "bf16", "fp16", "fp32"], help="auto: bf16 em Ampere+ (3060), fp16 na T4") t.add_argument("--sample-prompt", default="The capital of France is") t.add_argument("--grad-ckpt", action="store_true") t.add_argument("--compile", action="store_true") t.add_argument("--seed", type=int, default=0) t.add_argument("--push-to", default=None, help="ex: DedeProGames/Kiyo-Diffusion-135M") t.add_argument("--public", action="store_true", help="repo publico (padrao: privado)") g = sub.add_parser("generate") g.add_argument("--model", required=True) g.add_argument("--prompt", default="The cat sat on the") g.add_argument("--gen-len", type=int, default=128, help="tokens novos (0 = so preencher os [MASK] do prompt)") g.add_argument("--block-len", type=int, default=32, help="= gen-len p/ difusao pura, sem blocos") g.add_argument("--steps", type=int, default=None, help="padrao = 1 token por step") g.add_argument("--temperature", type=float, default=0.6) g.add_argument("--rep-penalty", type=float, default=1.1) g.add_argument("--no-bos", action="store_true") g.add_argument("--seed", type=int, default=0) g.add_argument("--watch", action="store_true", help="mostra o texto sendo 'revelado'") s = sub.add_parser("score", help="log-verossimilhanca de alternativas (multipla escolha)") s.add_argument("--model", required=True) s.add_argument("--context", required=True) s.add_argument("--choices", nargs="+", required=True) s.add_argument("--n-mc", type=int, default=32) s.add_argument("--seed", type=int, default=0) a = ap.parse_args() {"convert": cmd_convert, "check": cmd_check, "train": cmd_train, "generate": cmd_generate, "score": cmd_score}[a.cmd](a) if __name__ == "__main__": main()