mul / models /subdivider.py
MayMeHappy's picture
Upload folder using huggingface_hub
90ca518 verified
Raw History Blame Contribute Delete
53.3 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from einops import rearrange
from modules.attention import MLA
from .point_conditioner import PointConditioner
try:
from flash_attn import flash_attn_varlen_func
except Exception:
try:
from flash_attn.flash_attn_interface import flash_attn_varlen_func
except Exception:
flash_attn_varlen_func = None
class SinusoidalPosEmb(nn.Module):
def __init__(self, dim, scale=100.0):
super().__init__()
self.dim = dim
self.scale = scale
def forward(self, x):
device = x.device
dtype = x.dtype
x = x.float() * self.scale
half_dim = self.dim // 2
emb = math.log(10000) / (half_dim - 1)
emb = torch.exp(torch.arange(half_dim, device=device) * -emb)
emb = x[:, None] * emb[None, :]
emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
return emb.to(dtype=dtype)
class RMSNorm(nn.Module):
"""现代 LLM 标配:去均值的 RMSNorm"""
def __init__(self, dim, eps=1e-6):
super().__init__()
self.eps = eps
# 只需要学习一个缩放参数,去掉 bias
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
# 计算均方根 (Root Mean Square)
norm_x = torch.mean(x.pow(2), dim=-1, keepdim=True)
x_normed = x * torch.rsqrt(norm_x + self.eps)
return self.weight * x_normed
class SwiGLU(nn.Module):
"""现代 LLM 标配:SwiGLU 激活的门控 FFN"""
def __init__(self, d_model, hidden_dim=None):
super().__init__()
# 将原先 4 * d_model 的维度缩小到 8/3,保持参数量对等; hidden_dim 可显式覆盖
# (如 1024 宽度时 8/3 给 2730 非 256 倍数, 显式取 2816 对齐 Tensor Core)
hidden_dim = int((8 / 3) * d_model) if hidden_dim is None else int(hidden_dim)
# 提示:在实际工程中,通常会把 hidden_dim 向上取整到 128 或 256 的倍数,以最大化 Tensor Core 效率
self.w1 = nn.Linear(d_model, hidden_dim, bias=False)
self.w2 = nn.Linear(d_model, hidden_dim, bias=False)
self.w3 = nn.Linear(hidden_dim, d_model, bias=False)
def forward(self, x):
# 公式: Swish(xW1) ⊗ (xW2) -> W3
# PyTorch 中 F.silu 就是 Swish
return self.w3(F.silu(self.w1(x)) * self.w2(x))
class ModernAttention(nn.Module):
def __init__(self, d_model, nhead, dropout=0., qk_norm=False):
super().__init__()
assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
self.nhead = nhead
self.d_head = d_model // nhead
# 现代架构通常在 Attention 的投影层中也丢弃 bias
self.q_proj = nn.Linear(d_model, d_model, bias=False)
self.k_proj = nn.Linear(d_model, d_model, bias=False)
self.v_proj = nn.Linear(d_model, d_model, bias=False)
self.out_proj = nn.Linear(d_model, d_model, bias=False)
self.dropout = dropout
# QK-norm: 逐 head 对 Q/K 在 head_dim 上做 RMSNorm(在 RoPE 之前),稳定 attention logits、
# 防幅度爆炸。默认关(不改变既有模型);flow 等新模型可开。新增参数 -> 旧 ckpt load_strict=False。
self.qk_norm = qk_norm
if qk_norm:
self.q_norm = RMSNorm(self.d_head)
self.k_norm = RMSNorm(self.d_head)
def forward(self, q, k, v, mask=None, is_causal=False, rope_apply_fn=None):
B, L_q, _ = q.size()
_, L_k, _ = k.size()
# 投影并调整形状为 [Batch, Head, SeqLen, HeadDim]
Q = self.q_proj(q).view(B, L_q, self.nhead, self.d_head).transpose(1, 2)
K = self.k_proj(k).view(B, L_k, self.nhead, self.d_head).transpose(1, 2)
V = self.v_proj(v).view(B, L_k, self.nhead, self.d_head).transpose(1, 2)
if self.qk_norm: # QK-norm(RoPE 前),保持 dtype 供 SDPA/flash
Q = self.q_norm(Q).to(Q.dtype); K = self.k_norm(K).to(K.dtype)
# 注入旋转位置编码 (RoPE)
if rope_apply_fn is not None:
Q, K = rope_apply_fn(Q, K)
# 使用 PyTorch 2.0+ 内置的 SDPA,它会自动调用 Flash Attention 节省巨量显存
out = F.scaled_dot_product_attention(
Q, K, V,
attn_mask=mask,
dropout_p=self.dropout if self.training else 0.0,
is_causal=is_causal
)
# 恢复形状并输出
out = out.transpose(1, 2).contiguous().view(B, L_q, -1)
return self.out_proj(out)
def forward_varlen(self, q, k, v, lengths_q, lengths_k=None, is_causal=False, rope_apply_fn=None,
max_len_q=None, max_len_k=None):
"""max_len_q / max_len_k: flash 需要的 max_seqlen。**传 python int 可避免 GPU->CPU 同步**。
不传时回退到 `int(lengths.max().item())` —— 那是一次 cudaStreamSynchronize: CPU 必须等
GPU 把已排队的活干完才能读到值, 于是无法提前排队 kernel。本模型一次前向有 36 次 attention
调用(18 self + 18 cross), 即 72 次同步, 是「kernel 又多又小、GPU 有空隙」的直接成因,
也让 torch.compile 在这里 graph break(数据依赖的 unbacked symint)。
调用方本来就知道这些长度(lengths 是从 [c.numel() for c in coord_list] 建的), 应当传进来。
"""
if lengths_k is None:
lengths_k = lengths_q
if max_len_k is None:
max_len_k = max_len_q
total_q = q.size(0)
total_k = k.size(0)
Q = self.q_proj(q).view(total_q, self.nhead, self.d_head)
K = self.k_proj(k).view(total_k, self.nhead, self.d_head)
V = self.v_proj(v).view(total_k, self.nhead, self.d_head)
if self.qk_norm: # QK-norm(RoPE 前),.to 保持 dtype 供 flash-attn
Q = self.q_norm(Q).to(Q.dtype); K = self.k_norm(K).to(K.dtype)
if rope_apply_fn is not None:
Q, K = rope_apply_fn(Q, K)
dropout_p = self.dropout if self.training else 0.0
if can_use_flash_attn(Q):
out = flash_attn_varlen_func(
Q.contiguous(),
K.contiguous(),
V.contiguous(),
lengths_to_cu_seqlens(lengths_q),
lengths_to_cu_seqlens(lengths_k),
int(lengths_q.max().item()) if max_len_q is None else int(max_len_q),
int(lengths_k.max().item()) if max_len_k is None else int(max_len_k),
dropout_p=dropout_p,
causal=is_causal,
)
else:
out_chunks = []
q_start = 0
k_start = 0
for q_len, k_len in zip(lengths_q.tolist(), lengths_k.tolist()):
q_end = q_start + q_len
k_end = k_start + k_len
q_i = Q[q_start:q_end].transpose(0, 1).unsqueeze(0)
k_i = K[k_start:k_end].transpose(0, 1).unsqueeze(0)
v_i = V[k_start:k_end].transpose(0, 1).unsqueeze(0)
out_i = F.scaled_dot_product_attention(
q_i,
k_i,
v_i,
dropout_p=dropout_p,
is_causal=is_causal,
)
out_chunks.append(out_i.squeeze(0).transpose(0, 1))
q_start = q_end
k_start = k_end
out = torch.cat(out_chunks, dim=0)
return self.out_proj(out.contiguous().view(total_q, -1))
# class CrossAttnTransformerBlock(nn.Module):
# def __init__(self, d_model, nhead, dropout=0.):
# super().__init__()
# self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
# self.cross_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
# self.norm1 = nn.LayerNorm(d_model)
# self.norm2 = nn.LayerNorm(d_model)
# self.norm3 = nn.LayerNorm(d_model)
# self.ffn = nn.Sequential(
# nn.Linear(d_model, d_model * 4),
# nn.GELU(),
# nn.Dropout(dropout),
# nn.Linear(d_model * 4, d_model),
# )
# self.dropout = nn.Dropout(dropout)
# def forward(self, x, cond):
# # 1) token self-attention
# h = self.norm1(x)
# sa_out, _ = self.self_attn(h, h, h, need_weights=False)
# x = x + self.dropout(sa_out)
# # 2) cross-attention: query from current tokens, key/value from condition tokens
# h = self.norm2(x)
# ca_out, _ = self.cross_attn(h, cond, cond, need_weights=False)
# x = x + self.dropout(ca_out)
# # 3) feed-forward
# h = self.norm3(x)
# ff_out = self.ffn(h)
# x = x + self.dropout(ff_out)
# return x
class ModernCrossAttnTransformerBlock(nn.Module):
def __init__(self, d_model, nhead, dropout=0.):
super().__init__()
# 替换为支持 Flash Attention 的现代 Attention
self.self_attn = ModernAttention(d_model, nhead, dropout=dropout)
self.cross_attn = ModernAttention(d_model, nhead, dropout=dropout)
# 替换 LayerNorm 为 RMSNorm
self.norm1 = RMSNorm(d_model)
self.norm2 = RMSNorm(d_model)
self.norm3 = RMSNorm(d_model)
# 替换传统 FFN 为 SwiGLU
self.ffn = SwiGLU(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, cond, rope_apply_fn=None, causal_mask=False):
# 1) token self-attention
# 你的 Pre-Norm 逻辑保持不变,这是正确的
h = self.norm1(x)
# 注意:在这里把 RoPE 传进去处理 Query 和 Key
sa_out = self.self_attn(h, h, h, is_causal=causal_mask, rope_apply_fn=rope_apply_fn)
x = x + self.dropout(sa_out)
# 2) cross-attention: query from current tokens, key/value from condition tokens
if cond is not None:
h = self.norm2(x)
# Cross Attention 一般不使用 RoPE,因为输入源不同
ca_out = self.cross_attn(h, cond, cond)
x = x + self.dropout(ca_out)
# 3) feed-forward (SwiGLU)
h = self.norm3(x)
ff_out = self.ffn(h)
x = x + self.dropout(ff_out)
return x
def forward_varlen(self, x, lengths, cond=None, cond_lengths=None, rope_apply_fn=None, causal_mask=False):
h = self.norm1(x)
sa_out = self.self_attn.forward_varlen(
h,
h,
h,
lengths,
is_causal=causal_mask,
rope_apply_fn=rope_apply_fn,
)
x = x + self.dropout(sa_out)
if cond is not None:
h = self.norm2(x)
ca_out = self.cross_attn.forward_varlen(
h,
cond,
cond,
lengths,
lengths_k=cond_lengths,
)
x = x + self.dropout(ca_out)
h = self.norm3(x)
ff_out = self.ffn(h)
x = x + self.dropout(ff_out)
return x
def rotate_half(x):
"""
旋转特征的一半维度。
对于向量 [x1, x2, ..., x_d/2, x_d/2+1, ..., x_d],
将其转换为 [-x_d/2+1, ..., -x_d, x1, x2, ..., x_d/2]。
这在数学上等价于复数平面上的乘以 i。
"""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(q, k, cos, sin):
"""
将预计算好的 cos 和 sin 注入到 Query 和 Key 中。
q, k 形状期望为: (Batch, Num_Heads, Seq_Len, Head_Dim)
cos, sin 形状期望为: (1, 1, Seq_Len, Head_Dim)
"""
q_embed = (q * cos) + (rotate_half(q) * sin)
k_embed = (k * cos) + (rotate_half(k) * sin)
return q_embed, k_embed
def apply_rotary_pos_emb_varlen(q, k, cos, sin):
"""RoPE。**cos/sin 先转成 q/k 的 dtype**,保证输出 dtype 不被 cos/sin 带跑。
为什么必须这么写:cos/sin 由 `rotary_emb.forward_positions(..., dtype=x.dtype)` 生成,而
9 尺度的 rope_fn 是在 `x = raw_emb + refine_emb` 之后立刻构造的 —— `nn.Embedding` 不在 autocast
的转型名单里,那时 x 还是 **fp32**,于是 cos/sin 是 fp32。bf16 的 Q/K 乘上 fp32 的 cos/sin 会
类型提升成 fp32,`can_use_flash_attn` 判否,直接掉进 `ModernAttention.forward_varlen` 里
**SDPA 的逐序列 python 循环**。命中的恰好是 enc_blocks_9 / dec_blocks_9 这 4 层,
也就是跑在满 token 长度(其余层已被 hourglass 降到 1/3、1/9)上最贵的 4 个注意力。
3 尺度与 bottleneck 的 rope_fn 因为构造时 x 已是 bf16,本来就走 flash —— 即同一网络里
4 层 fp32 慢路径 + 14 层 bf16 快路径,不一致。统一到 bf16。
"""
cos_q, sin_q = cos.to(q.dtype), sin.to(q.dtype)
q_embed = (q * cos_q) + (rotate_half(q) * sin_q)
cos_k, sin_k = (cos_q, sin_q) if k.dtype == q.dtype else (cos.to(k.dtype), sin.to(k.dtype))
k_embed = (k * cos_k) + (rotate_half(k) * sin_k)
return q_embed, k_embed
def lengths_to_cu_seqlens(lengths):
lengths = lengths.to(dtype=torch.int32)
prefix = torch.empty(lengths.numel() + 1, device=lengths.device, dtype=torch.int32)
prefix[0] = 0
prefix[1:] = torch.cumsum(lengths, dim=0, dtype=torch.int32)
return prefix.contiguous()
def lengths_to_position_ids(lengths):
total = int(lengths.sum().item())
if total == 0:
return torch.empty(0, device=lengths.device, dtype=torch.long)
starts = torch.cumsum(lengths, dim=0) - lengths
return torch.arange(total, device=lengths.device, dtype=torch.long) - torch.repeat_interleave(starts, lengths)
def can_use_flash_attn(x):
return (
flash_attn_varlen_func is not None
and x.is_cuda
and x.dtype in (torch.float16, torch.bfloat16)
)
class RotaryEmbedding(nn.Module):
"""
预计算和缓存 RoPE 的 cos 和 sin 矩阵。
"""
def __init__(self, dim, max_position_embeddings=90000, base=10000.0, device=None):
super().__init__()
self.dim = dim
self.max_position_embeddings = max_position_embeddings
self.base = base
# 计算旋转频率: theta_i = 10000^(-2(i-1)/d)
inv_freq = 1.0 / (self.base ** (torch.arange(0, self.dim, 2, dtype=torch.float32, device=device) / self.dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)
# 预先构建缓存
self._set_cos_sin_cache(seq_len=max_position_embeddings, device=self.inv_freq.device, dtype=torch.get_default_dtype())
def _set_cos_sin_cache(self, seq_len, device, dtype):
self.max_seq_len_cached = seq_len
t = torch.arange(self.max_seq_len_cached, device=device, dtype=torch.float32)
# freqs 形状: (seq_len, dim/2)
freqs = torch.outer(t, self.inv_freq)
# 将频率在特征维度上复制一份,形状变为: (seq_len, dim)
# 前半部分和后半部分的频率一致,为了和 rotate_half 的逻辑对齐
emb = torch.cat((freqs, freqs), dim=-1)
# 增加 Batch 和 Head 的广播维度: (1, 1, Seq_Len, Head_Dim)
self.register_buffer("cos_cached", emb.cos().to(dtype).unsqueeze(0).unsqueeze(0), persistent=False)
self.register_buffer("sin_cached", emb.sin().to(dtype).unsqueeze(0).unsqueeze(0), persistent=False)
def forward(self, x, seq_len=None):
"""
x 形状: (Batch, Num_Heads, Seq_Len, Head_Dim)
"""
# 如果当前序列长度超过了预计算的缓存,则动态扩展缓存
if seq_len > self.max_seq_len_cached:
self._set_cos_sin_cache(seq_len=seq_len, device=x.device, dtype=x.dtype)
# 截取当前需要的序列长度返回
return (
self.cos_cached[:, :, :seq_len, ...].to(dtype=x.dtype),
self.sin_cached[:, :, :seq_len, ...].to(dtype=x.dtype),
)
def forward_positions(self, position_ids, dtype=None):
if dtype is None:
dtype = self.cos_cached.dtype
if position_ids.numel() == 0:
empty = torch.empty(0, 1, self.dim, device=position_ids.device, dtype=dtype)
return empty, empty
seq_len = int(position_ids.max().item()) + 1
if seq_len > self.max_seq_len_cached:
self._set_cos_sin_cache(seq_len=seq_len, device=position_ids.device, dtype=dtype)
cos = self.cos_cached[0, 0, position_ids, :].to(device=position_ids.device, dtype=dtype)
sin = self.sin_cached[0, 0, position_ids, :].to(device=position_ids.device, dtype=dtype)
return cos.unsqueeze(1), sin.unsqueeze(1)
class LinearDownsample(nn.Module):
def __init__(self, dim, shorten_factor):
super().__init__()
self.proj = nn.Linear(dim * shorten_factor, dim)
self.shorten_factor = shorten_factor
def forward(self, x):
x = rearrange(x, 'b (n s) d -> b n (s d)', s=self.shorten_factor)
return self.proj(x)
def forward_varlen(self, x, lengths):
if torch.any(lengths % self.shorten_factor != 0):
raise ValueError(f"All sequence lengths must be divisible by {self.shorten_factor} for hourglass downsample.")
x = rearrange(x, '(n s) d -> n (s d)', s=self.shorten_factor)
return self.proj(x), lengths // self.shorten_factor
class LinearUpsample(nn.Module):
def __init__(self, dim, shorten_factor):
super().__init__()
self.proj = nn.Linear(dim, dim * shorten_factor)
self.shorten_factor = shorten_factor
def forward(self, x):
x = self.proj(x)
return rearrange(x, 'b n (s d) -> b (n s) d', s=self.shorten_factor)
def forward_varlen(self, x, lengths):
x = self.proj(x)
x = rearrange(x, 'n (s d) -> (n s) d', s=self.shorten_factor)
return x, lengths * self.shorten_factor
class DiscreteDiffusionTransformer(nn.Module):
def __init__(self,
vocab_A,
vocab_B,
max_seq_len,
d_model=768,
nhead=12,
depth = [4,4,8],
dropout=0.,
encoder_freeze=True,
encoder_name='miche-256-feature',
use_pc=True,
time_embed_scale=100.0):
super().__init__()
self.vocab_A = vocab_A
self.vocab_B = vocab_B
self.d_model = d_model
# MASK Token 的 ID,我们将其设为 B(因为真实的 refine_token 是 0 到 B-1)
self.mask_token_id = vocab_B
# 1. 词嵌入 (Embeddings)
self.raw_embedding = nn.Embedding(vocab_A, d_model)
# refine_token 的词表大小是 B + 1 (包含了 MASK token)
self.refine_embedding = nn.Embedding(vocab_B + 1, d_model)
# 2. 位置编码 (Positional Encoding)
# self.pos_embedding = nn.Embedding(max_seq_len, d_model)
# 3. 时间步编码 (Timestep Embedding)
self.time_mlp = nn.Sequential(
SinusoidalPosEmb(d_model, scale=time_embed_scale),
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model)
)
# 4. 主干网络 (Denoising Backbone)
# U-Net/Hourglass 主干:
# enc(9) -> down -> enc(3) -> down -> bottleneck(1) -> up+skip -> dec(3) -> up+skip -> dec(9)
self.depth = depth
self.enc_blocks_9 = nn.ModuleList(
[ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[0])]
)
self.enc_blocks_3 = nn.ModuleList(
[ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[1])]
)
self.bottleneck_blocks = nn.ModuleList(
[ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[2])]
)
self.dec_blocks_3 = nn.ModuleList(
[ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[1])]
)
self.dec_blocks_9 = nn.ModuleList(
[ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[0])]
)
self.down_9_to_3 = LinearDownsample(d_model, shorten_factor=3)
self.down_3_to_1 = LinearDownsample(d_model, shorten_factor=3)
self.up_1_to_3 = LinearUpsample(d_model, shorten_factor=3)
self.up_3_to_9 = LinearUpsample(d_model, shorten_factor=3)
self.rotary_emb = RotaryEmbedding(dim=d_model//nhead, max_position_embeddings=max_seq_len)
# 5. 输出预测头 (只预测真实的 B 个 Token)
self.lm_head = nn.Linear(d_model, vocab_B)
self.use_pc = use_pc
if use_pc:
print(f'Point cloud encoder: {encoder_name} | freeze: {encoder_freeze}')
self.conditioner = PointConditioner(model_name=encoder_name, freeze=encoder_freeze, feature_dim=d_model)
# Learnable null condition for classifier-free guidance.
# Replaces self.conditioner(pc) when pc is None, so the model can learn
# an "unconditional" representation during cond-drop fine-tuning.
self.null_cond_len = 257 # matches PointConditioner.cond_length
self.null_cond = nn.Parameter(torch.randn(self.null_cond_len, d_model) * 0.02)
def forward(self, raw_tokens, noisy_refine_tokens, t, pc=None):
"""
raw_tokens: [b, n], 取值 0~A-1
noisy_refine_tokens: [b, n], 取值 0~B (B是MASK)
t: [b], 当前的时间步
pc: [b, n, 6], 点云数据
"""
if isinstance(raw_tokens, (list, tuple)):
return self.forward_varlen(raw_tokens, noisy_refine_tokens, t, pc)
b, n = raw_tokens.shape
device = raw_tokens.device
# 获取 Embedding
raw_emb = self.raw_embedding(raw_tokens) # [b, n, d_model]
refine_emb = self.refine_embedding(noisy_refine_tokens) # [b, n, d_model]
# 获取位置和时间 Embedding
time_emb = self.time_mlp(t).unsqueeze(1) # [b, 1, d_model]
# x 作为被去噪序列,cond 作为每层 cross-attn 的条件序列
x = raw_emb + refine_emb + time_emb
if self.use_pc:
if pc is None:
# Classifier-free guidance / cond-drop: use learnable null condition.
cond = self.null_cond.unsqueeze(0).expand(b, -1, -1)
else:
cond = self.conditioner(pc) # [b, 257, d_model]
else:
cond = None
if n % 9 != 0:
raise ValueError(f"Sequence length {n} is not compatible with 9->3->1 hourglass compression.")
dummy_tensor = torch.empty(1, device=x.device, dtype=torch.float32)
def rope_fn_for(seq_len):
cos, sin = self.rotary_emb(dummy_tensor, seq_len=seq_len)
return lambda q, k: apply_rotary_pos_emb(q, k, cos, sin)
# Encoder @ length 9
rope_fn_9 = rope_fn_for(x.shape[1])
for block in self.enc_blocks_9:
x = block(x, cond, rope_apply_fn=rope_fn_9)
skip_9 = x
# Downsample 9 -> 3
x = self.down_9_to_3(x)
# Encoder @ length 3
rope_fn_3 = rope_fn_for(x.shape[1])
for block in self.enc_blocks_3:
x = block(x, cond, rope_apply_fn=rope_fn_3)
skip_3 = x
# Downsample 3 -> 1 (or in general: n/3 -> n/9)
x = self.down_3_to_1(x)
# Bottleneck
rope_fn_1 = rope_fn_for(x.shape[1])
for block in self.bottleneck_blocks:
x = block(x, cond, rope_apply_fn=rope_fn_1)
# Upsample 1 -> 3 and long skip connection
x = self.up_1_to_3(x)
x = x + skip_3
# Decoder @ length 3
for block in self.dec_blocks_3:
x = block(x, cond, rope_apply_fn=rope_fn_3)
# Upsample 3 -> 9 and long skip connection
x = self.up_3_to_9(x)
x = x + skip_9
# Decoder @ length 9
for block in self.dec_blocks_9:
x = block(x, cond, rope_apply_fn=rope_fn_9)
# 预测 Logits
logits = self.lm_head(x) # [b, n, vocab_B]
return logits
def forward_varlen(self, raw_tokens, noisy_refine_tokens, t, pc=None):
device = t.device
raw_tokens = [seq.to(device=device, dtype=torch.long) for seq in raw_tokens]
noisy_refine_tokens = [seq.to(device=device, dtype=torch.long) for seq in noisy_refine_tokens]
lengths = torch.tensor([seq.numel() for seq in raw_tokens], device=device, dtype=torch.long)
if len(raw_tokens) == 0 or int(lengths.sum().item()) == 0:
raise ValueError("forward_varlen received an empty batch.")
if any(a.numel() != b.numel() for a, b in zip(raw_tokens, noisy_refine_tokens)):
raise ValueError("raw token lengths and refine token lengths must match per sample.")
if torch.any(lengths % 9 != 0):
bad = lengths[lengths % 9 != 0].detach().cpu().tolist()
raise ValueError(f"Varlen hourglass requires each sequence length to be divisible by 9, got {bad}.")
raw_packed = torch.cat(raw_tokens, dim=0)
refine_packed = torch.cat(noisy_refine_tokens, dim=0)
raw_emb = self.raw_embedding(raw_packed)
refine_emb = self.refine_embedding(refine_packed)
time_emb = self.time_mlp(t).repeat_interleave(lengths, dim=0)
x = raw_emb + refine_emb + time_emb
if self.use_pc:
if pc is None:
# Null condition path for CFG / cond-drop. Replicate the learned
# null_cond per sample and flatten to match the varlen layout.
bsz = len(raw_tokens)
null = self.null_cond # [257, d_model]
cond = null.unsqueeze(0).expand(bsz, -1, -1).reshape(-1, null.size(-1))
cond_lengths = torch.full((bsz,), self.null_cond_len, device=device, dtype=torch.long)
else:
pc = pc.to(device=device)
cond = self.conditioner(pc)
cond_lengths = torch.full((cond.size(0),), cond.size(1), device=device, dtype=torch.long)
cond = cond.reshape(-1, cond.size(-1))
else:
cond = None
cond_lengths = None
def rope_fn_for(cur_lengths):
position_ids = lengths_to_position_ids(cur_lengths)
cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)
rope_fn_9 = rope_fn_for(lengths)
for block in self.enc_blocks_9:
x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
skip_9 = x
lengths_9 = lengths
x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
rope_fn_3 = rope_fn_for(lengths)
for block in self.enc_blocks_3:
x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
skip_3 = x
lengths_3 = lengths
x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
rope_fn_1 = rope_fn_for(lengths)
for block in self.bottleneck_blocks:
x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_1)
x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
if not torch.equal(lengths, lengths_3):
raise RuntimeError("Internal length mismatch after 1->3 upsample.")
x = x + skip_3
for block in self.dec_blocks_3:
x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
if not torch.equal(lengths, lengths_9):
raise RuntimeError("Internal length mismatch after 3->9 upsample.")
x = x + skip_9
for block in self.dec_blocks_9:
x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
return self.lm_head(x)
def cosine_schedule(t, T):
"""
余弦调度函数。
t: 当前进度步数 (0 表示起始/全噪,T 表示结束/真实数据)
返回: 需要 mask 的比例 (t=0时返回1.0,t=T时返回0.0)
"""
# 确保 t 是浮点数以进行安全除法
return torch.cos((t / T) * (math.pi / 2))
return torch.cos((t / T) * (math.pi / 2))
def precompute_topology_tensors(raw_faces, tokens_per_face=9):
"""
预处理:完全向量化的拓扑索引构建(在去噪循环外执行一次即可)
返回的是一维的张量,方便在 GPU 上做极速的 Scatter/Gather 计算
"""
B, num_faces, _, _ = raw_faces.shape
device = raw_faces.device
# 转为 Python list 加速哈希构建
raw_faces_list = raw_faces.cpu().tolist()
token_indices_list = []
v_indices_list = []
v_shared_count = 0
for b in range(B):
vertex_dict = {}
for f in range(num_faces):
for v_idx in range(3):
coord = tuple(raw_faces_list[b][f][v_idx])
# 计算展平后的【全局绝对 Token 索引】
global_token_idx = b * (num_faces * tokens_per_face) + f * tokens_per_face + v_idx
if coord not in vertex_dict:
vertex_dict[coord] = []
vertex_dict[coord].append(global_token_idx)
# 仅保留被共享的顶点
for indices in vertex_dict.values():
if len(indices) > 1:
token_indices_list.extend(indices)
v_indices_list.extend([v_shared_count] * len(indices)) # 记录属于哪个全局共享顶点 ID
v_shared_count += 1
# 如果没有任何共享顶点,直接返回空
if len(token_indices_list) == 0:
return None, None, None, 0
# [M] 个共享 Token 的绝对索引
token_idx = torch.tensor(token_indices_list, dtype=torch.long, device=device)
# [M] 每个 Token 对应的全局顶点 ID
v_idx = torch.tensor(v_indices_list, dtype=torch.long, device=device)
# [M] 每个 Token 对应的绝对面片(Face)索引
face_idx = token_idx // tokens_per_face
return token_idx, v_idx, face_idx, v_shared_count
def sample_refine_tokens_with_topology(
model,
mask_token_id,
raw_tokens,
pc,
T_infer=10,
T_train=100,
tokens_per_face=9,
use_gumbel=False,
gumbel_temp_init=1.0,
use_soft_consensus=False,
consensus_start_step_ratio=0.0,
cfg_scale=0.0,
null_pc=None,
diag_stats=None,
forbidden_token_ids=(0,),
forbid_mask_token=True,
empty_token_ids=(),
empty_token_bias=0.0,
):
"""
极致优化的拓扑一致性采样代码(零 Python Loop)
empty_token_ids / empty_token_bias: 对"空/背景"占用 token(实测 258,770 各~33%)的 logits
减去 empty_token_bias(负偏置)。>0 -> 空 token 更不易被选 -> 更密的细分(更多面)。默认 no-op。
优化 flag:
use_gumbel : 重 mask 时给 confidence 加温度退火的 Gumbel 噪声 (MaskGIT 标配)
gumbel_temp_init : 初始温度,随 step 线性退火到 0
use_soft_consensus : 共享顶点用概率分布聚合代替硬多数表决
consensus_start_step_ratio: 在 step / T_infer 达到该值前不做 topology consensus
cfg_scale : Classifier-free guidance 强度. 0 = 不开启 (单 forward).
>0 时每步多一次 forward (null_pc 作 uncond).
logits = logits_c + cfg_scale * (logits_c - logits_u)
null_pc : (B, N, C) 作为 uncond 输入. 若为 None, 用 zeros_like(pc).
注: 当前模型未训练 cond-drop, 用 null pc 是 OOD 行为, 效果不保证.
diag_stats : 可选 dict, 把每步的诊断信息追加进去 (供测试用)
forbidden_token_ids : 禁止模型"决定"采样到的 token id 集合. 这些 id 的 logits
在 softmax 前被设为 -inf, 后续 argmax / consensus 全部
自动避开. 默认 (0,) 即 pad/EOS——堵住"模型中段吐出 0
-> save_stage_prediction 截断 -> mesh 只剩前半段"这条 bug.
forbid_mask_token : 同时也禁止预测 mask_token_id (默认 True). mask 是占位符,
模型不应把它当最终答案.
"""
model.eval()
b, n = raw_tokens.shape
device = raw_tokens.device
num_faces = n // tokens_per_face
# 收集要屏蔽的 token id; 真正裁剪并搬到 GPU 留到拿到 logits 后再做
# (mask_token_id 通常等于 vocab_B, 不在输出 logits 的维度内, 直接索引会越界)
_forbidden_raw = list(forbidden_token_ids) if forbidden_token_ids is not None else []
if forbid_mask_token:
_forbidden_raw.append(int(mask_token_id))
_forbidden_raw = sorted(set(int(x) for x in _forbidden_raw))
forbidden_t = None # 在首次 forward 后用 vocab_size 过滤构建
# --- 1. 预处理 (仅执行一次,耗时极短) ---
raw_faces = raw_tokens.view(b, num_faces, 3, 3)
token_idx, v_idx, face_idx, v_shared_count = precompute_topology_tensors(raw_faces, tokens_per_face)
# 初始化
current_tokens = torch.full((b, n), mask_token_id, dtype=torch.long, device=device)
unmasked_flags = torch.zeros((b, n), dtype=torch.bool, device=device)
vocab_size = None # 将在第一次 forward 后获取
def cosine_schedule(t, max_val):
return torch.cos(t * torch.pi * 0.5) * max_val
# 预备 null_pc (CFG)
# 三种模式:
# - null_pc 是 tensor : 用该 tensor 作为 uncond 输入(老模型可用 zeros_like(pc))
# - null_pc == "learned" : 调用 model(..., pc=None), 走模型的 self.null_cond 路径
# (只有 cond-drop fine-tune 过的模型才会有意义)
# - null_pc is None (默认): 用 zeros_like(pc), 行为同老版
use_learned_null = (isinstance(null_pc, str) and null_pc.lower() == "learned")
if cfg_scale > 0.0 and not use_learned_null:
if null_pc is None:
null_pc = torch.zeros_like(pc)
null_pc = null_pc.to(device=device, dtype=pc.dtype)
for step in range(T_infer):
current_progress_ratio = step / T_infer
t_model = torch.full((b,), current_progress_ratio, device=device, dtype=torch.float)
# [1] 前向预测 (可选 CFG: 双 forward 线性外推)
logits_c = model(raw_tokens, current_tokens, t_model, pc) # [b, n, vocab_size]
if cfg_scale > 0.0:
if use_learned_null:
logits_u = model(raw_tokens, current_tokens, t_model, None)
else:
logits_u = model(raw_tokens, current_tokens, t_model, null_pc)
logits = logits_c + cfg_scale * (logits_c - logits_u)
else:
logits = logits_c
if vocab_size is None:
vocab_size = logits.shape[-1]
# 现在知道真实输出 vocab 大小, 把越界的 id (例如 mask_token_id=vocab_B)
# 剔除掉后再构建 forbidden_t. 否则 logits[..., id] 会触发 CUDA assert.
_valid = [x for x in _forbidden_raw if 0 <= x < vocab_size]
if len(_valid) > 0:
forbidden_t = torch.tensor(_valid, dtype=torch.long, device=device)
# 禁止采样到 pad / EOS. softmax 前置 -inf, 后续 argmax 与
# consensus 聚合 (软/硬) 都因 probs[forbidden]=0 自动避开.
if forbidden_t is not None:
logits[..., forbidden_t] = float('-inf')
# 空行/空占用 token 负偏置: 减小其 logits -> 更不易被选 -> 更密的细分
if empty_token_bias != 0.0 and len(empty_token_ids) > 0:
_et = torch.tensor([int(i) for i in empty_token_ids if 0 <= int(i) < vocab_size],
dtype=torch.long, device=device)
if _et.numel() > 0:
logits[..., _et] = logits[..., _et] - float(empty_token_bias)
probs = F.softmax(logits, dim=-1)
max_probs, pred_tokens = probs.max(dim=-1) # [b, n]
force_mask = torch.zeros((b, n), dtype=torch.bool, device=device)
target_vals = None # 最后一步若开启 consensus 会用到
# ---------------------------------------------------------
# [2] 拓扑共识 (硬多数表决 / 软概率聚合)
# 通过 consensus_start_step_ratio 控制何时开启
# ---------------------------------------------------------
apply_consensus = (v_shared_count > 0) and (current_progress_ratio >= consensus_start_step_ratio)
if apply_consensus:
if use_soft_consensus:
# 软共识: 直接 scatter_add 共享 token 的概率分布
shared_probs = probs.view(-1, vocab_size)[token_idx] # [M, V]
agg_probs = torch.zeros((v_shared_count, vocab_size), dtype=shared_probs.dtype, device=device)
v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
agg_probs.scatter_add_(0, v_idx_expanded, shared_probs)
mode_vals = agg_probs.argmax(dim=-1)
target_vals = mode_vals[v_idx]
preds_flat = pred_tokens.view(-1)
shared_vals = preds_flat[token_idx]
is_disagree = (shared_vals != target_vals)
else:
# 硬多数表决
preds_flat = pred_tokens.view(-1)
shared_vals = preds_flat[token_idx]
one_hot_vals = F.one_hot(shared_vals, num_classes=vocab_size) # [M, V]
votes = torch.zeros((v_shared_count, vocab_size), dtype=torch.long, device=device)
v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
votes.scatter_add_(0, v_idx_expanded, one_hot_vals)
mode_vals = votes.argmax(dim=-1)
target_vals = mode_vals[v_idx]
is_disagree = (shared_vals != target_vals)
if is_disagree.any():
disagree_faces = face_idx[is_disagree]
force_mask_faces = torch.zeros(b * num_faces, dtype=torch.bool, device=device)
force_mask_faces[disagree_faces] = True
force_mask_flat = force_mask_faces.repeat_interleave(tokens_per_face)
force_mask = force_mask_flat.view(b, n)
# ---------------------------------------------------------
# [3] 置信度干预与 Mask 更新
# ---------------------------------------------------------
# 注意: 这里先克隆,避免 Gumbel 噪声破坏掉之后我们对 unmasked/force 的 ±inf 标记
confidence = max_probs.clone()
if use_gumbel:
# 温度随进度线性退火到 0
temperature = gumbel_temp_init * (1.0 - current_progress_ratio)
if temperature > 0:
uniform = torch.rand_like(confidence).clamp_(1e-9, 1.0 - 1e-9)
gumbel = -torch.log(-torch.log(uniform))
confidence = confidence + temperature * gumbel
# 已确认的位置不应被再 mask
confidence[unmasked_flags] = float('inf')
if apply_consensus:
confidence[force_mask] = -float('inf')
unmasked_flags = unmasked_flags & (~force_mask)
# 诊断信息
if diag_stats is not None:
diag_stats.setdefault('force_count', []).append(int(force_mask.sum().item()))
diag_stats.setdefault('avg_conf', []).append(float(max_probs.mean().item()))
if step == T_infer - 1:
# 最后一步:用共识算出的最终值硬覆盖所有可能残余的不一致
if apply_consensus and target_vals is not None:
preds_flat = pred_tokens.view(-1)
preds_flat[token_idx] = target_vals
pred_tokens = preds_flat.view(b, n)
current_tokens = torch.where(unmasked_flags, current_tokens, pred_tokens)
else:
next_progress_ratio = (step + 1) / T_infer
ratio = cosine_schedule(torch.tensor(next_progress_ratio), 1.0).item()
num_to_mask = int(ratio * n)
max_forced_in_batch = int(force_mask.sum(dim=-1).max().item()) if apply_consensus else 0
actual_k = min(max(num_to_mask, max_forced_in_batch), n)
_, mask_indices = torch.topk(-confidence, k=actual_k, dim=-1)
new_mask = torch.zeros_like(unmasked_flags)
new_mask.scatter_(1, mask_indices, True)
# 兜底: force_mask 必入 new_mask (Gumbel 噪声不会让 -inf 漏选, 但保留以防边界条件)
new_mask = new_mask | force_mask
current_tokens = torch.where(new_mask, mask_token_id, pred_tokens)
unmasked_flags = ~new_mask
return current_tokens
def precompute_topology_tensors_fm(raw_faces, tokens_per_face=9):
"""precompute_topology_tensors 的扩展版: 除了 F(refine 位置 0/1/2 = 3 个顶点/父的占用,
按顶点 coord 分组), 额外把 intra(M_inner, refine 位置 3/4/5, 分别对应父 0/1/2 的内部连接
矩阵)也按其父 coord 分组。F 与 intra 用不同"通道标签"分开归组, 绝不互相表决。
token 布局(与 fix3 同构, 见 tokenizer decode 的 chunk[9..17]):
refine[0,1,2] = F0,F1,F2(父 0/1/2 占用) -> 按父 coord 分组(每父冗余, 应一致)
refine[3,4,5] = M00,M11,M22(父 0/1/2 intra)-> 按父 coord 分组(每父冗余, 应一致)
refine[6,7,8] = M01,M02,M12(inter, 每面本地)-> 不分组、不表决
返回 (token_idx, v_idx, face_idx, v_shared_count), 结构与原版完全一致, 可直接喂给
采样循环(F 组的众数落在 F 值域, intra 组的众数落在 intra 值域, 天然不串)。
"""
B, num_faces, _, _ = raw_faces.shape
device = raw_faces.device
raw_faces_list = raw_faces.cpu().tolist()
# 每个"冗余通道" = (refine 位置, 该位置对应的父编号, 通道标签)。
# F: 位置 p -> 父 p; intra: 位置 3+p -> 父 p。inter(6/7/8)不在其中。
channels = [(0, 0, "F"), (1, 1, "F"), (2, 2, "F"),
(3, 0, "I"), (4, 1, "I"), (5, 2, "I")]
token_indices_list = []
v_indices_list = []
v_shared_count = 0
for b in range(B):
group_dict = {}
for f in range(num_faces):
for pos, parent, tag in channels:
coord = tuple(raw_faces_list[b][f][parent]) # 该父的坐标
key = (tag, coord) # F / intra 分开归组
global_token_idx = b * (num_faces * tokens_per_face) + f * tokens_per_face + pos
group_dict.setdefault(key, []).append(global_token_idx)
for indices in group_dict.values():
if len(indices) > 1: # 仅保留被共享(>1 面引用)的
token_indices_list.extend(indices)
v_indices_list.extend([v_shared_count] * len(indices))
v_shared_count += 1
if len(token_indices_list) == 0:
return None, None, None, 0
token_idx = torch.tensor(token_indices_list, dtype=torch.long, device=device)
v_idx = torch.tensor(v_indices_list, dtype=torch.long, device=device)
face_idx = token_idx // tokens_per_face
return token_idx, v_idx, face_idx, v_shared_count
def sample_refine_tokens_with_topology_fm(
model,
mask_token_id,
raw_tokens,
pc,
T_infer=10,
T_train=100,
tokens_per_face=9,
use_gumbel=False,
gumbel_temp_init=1.0,
use_soft_consensus=False,
consensus_start_step_ratio=0.0,
cfg_scale=0.0,
null_pc=None,
diag_stats=None,
forbidden_token_ids=(0,),
forbid_mask_token=True,
):
"""sample_refine_tokens_with_topology 的"F + M_inner(intra)双对齐"版。
与原版唯一区别: 拓扑分组用 precompute_topology_tensors_fm ——
原版只把 refine 位置 0/1/2(F, 顶点占用)按共享顶点分组表决;
本版额外把 3/4/5(intra, 每父内部连接矩阵)也按共享父分组表决。
因此每步的共识检测/末步硬覆盖会同时作用在 F 与 intra 上, 任一不一致的面整块 9 token
重掩重预测。inter(6/7/8)每面本地、无冗余, 仍不表决(但会随所在面被连坐重掩)。
其余采样逻辑(CFG 双前向、forbidden、Gumbel、cosine 揭示)与原版逐行一致。
参数含义见 sample_refine_tokens_with_topology。
"""
model.eval()
b, n = raw_tokens.shape
device = raw_tokens.device
num_faces = n // tokens_per_face
_forbidden_raw = list(forbidden_token_ids) if forbidden_token_ids is not None else []
if forbid_mask_token:
_forbidden_raw.append(int(mask_token_id))
_forbidden_raw = sorted(set(int(x) for x in _forbidden_raw))
forbidden_t = None
# --- 1. 预处理: F + intra 双通道拓扑分组(唯一改动) ---
raw_faces = raw_tokens.view(b, num_faces, 3, 3)
token_idx, v_idx, face_idx, v_shared_count = precompute_topology_tensors_fm(raw_faces, tokens_per_face)
current_tokens = torch.full((b, n), mask_token_id, dtype=torch.long, device=device)
unmasked_flags = torch.zeros((b, n), dtype=torch.bool, device=device)
vocab_size = None
def cosine_schedule(t, max_val):
return torch.cos(t * torch.pi * 0.5) * max_val
use_learned_null = (isinstance(null_pc, str) and null_pc.lower() == "learned")
if cfg_scale > 0.0 and not use_learned_null:
if null_pc is None:
null_pc = torch.zeros_like(pc)
null_pc = null_pc.to(device=device, dtype=pc.dtype)
for step in range(T_infer):
current_progress_ratio = step / T_infer
t_model = torch.full((b,), current_progress_ratio, device=device, dtype=torch.float)
# [1] 前向预测 (可选 CFG)
logits_c = model(raw_tokens, current_tokens, t_model, pc)
if cfg_scale > 0.0:
if use_learned_null:
logits_u = model(raw_tokens, current_tokens, t_model, None)
else:
logits_u = model(raw_tokens, current_tokens, t_model, null_pc)
logits = logits_c + cfg_scale * (logits_c - logits_u)
else:
logits = logits_c
if vocab_size is None:
vocab_size = logits.shape[-1]
_valid = [x for x in _forbidden_raw if 0 <= x < vocab_size]
if len(_valid) > 0:
forbidden_t = torch.tensor(_valid, dtype=torch.long, device=device)
if forbidden_t is not None:
logits[..., forbidden_t] = float('-inf')
probs = F.softmax(logits, dim=-1)
max_probs, pred_tokens = probs.max(dim=-1)
force_mask = torch.zeros((b, n), dtype=torch.bool, device=device)
target_vals = None
# [2] 拓扑共识 (F + intra 同时, 因两者在 v_idx 里是不同组, 自动分别表决)
apply_consensus = (v_shared_count > 0) and (current_progress_ratio >= consensus_start_step_ratio)
if apply_consensus:
if use_soft_consensus:
shared_probs = probs.view(-1, vocab_size)[token_idx]
agg_probs = torch.zeros((v_shared_count, vocab_size), dtype=shared_probs.dtype, device=device)
v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
agg_probs.scatter_add_(0, v_idx_expanded, shared_probs)
mode_vals = agg_probs.argmax(dim=-1)
target_vals = mode_vals[v_idx]
preds_flat = pred_tokens.view(-1)
shared_vals = preds_flat[token_idx]
is_disagree = (shared_vals != target_vals)
else:
preds_flat = pred_tokens.view(-1)
shared_vals = preds_flat[token_idx]
one_hot_vals = F.one_hot(shared_vals, num_classes=vocab_size)
votes = torch.zeros((v_shared_count, vocab_size), dtype=torch.long, device=device)
v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
votes.scatter_add_(0, v_idx_expanded, one_hot_vals)
mode_vals = votes.argmax(dim=-1)
target_vals = mode_vals[v_idx]
is_disagree = (shared_vals != target_vals)
if is_disagree.any():
disagree_faces = face_idx[is_disagree]
force_mask_faces = torch.zeros(b * num_faces, dtype=torch.bool, device=device)
force_mask_faces[disagree_faces] = True
force_mask_flat = force_mask_faces.repeat_interleave(tokens_per_face)
force_mask = force_mask_flat.view(b, n)
# [3] 置信度干预与 Mask 更新
confidence = max_probs.clone()
if use_gumbel:
temperature = gumbel_temp_init * (1.0 - current_progress_ratio)
if temperature > 0:
uniform = torch.rand_like(confidence).clamp_(1e-9, 1.0 - 1e-9)
gumbel = -torch.log(-torch.log(uniform))
confidence = confidence + temperature * gumbel
confidence[unmasked_flags] = float('inf')
if apply_consensus:
confidence[force_mask] = -float('inf')
unmasked_flags = unmasked_flags & (~force_mask)
if diag_stats is not None:
diag_stats.setdefault('force_count', []).append(int(force_mask.sum().item()))
diag_stats.setdefault('avg_conf', []).append(float(max_probs.mean().item()))
if step == T_infer - 1:
if apply_consensus and target_vals is not None:
preds_flat = pred_tokens.view(-1)
preds_flat[token_idx] = target_vals
pred_tokens = preds_flat.view(b, n)
current_tokens = torch.where(unmasked_flags, current_tokens, pred_tokens)
else:
next_progress_ratio = (step + 1) / T_infer
ratio = cosine_schedule(torch.tensor(next_progress_ratio), 1.0).item()
num_to_mask = int(ratio * n)
max_forced_in_batch = int(force_mask.sum(dim=-1).max().item()) if apply_consensus else 0
actual_k = min(max(num_to_mask, max_forced_in_batch), n)
_, mask_indices = torch.topk(-confidence, k=actual_k, dim=-1)
new_mask = torch.zeros_like(unmasked_flags)
new_mask.scatter_(1, mask_indices, True)
new_mask = new_mask | force_mask
current_tokens = torch.where(new_mask, mask_token_id, pred_tokens)
unmasked_flags = ~new_mask
return current_tokens
def precompute_topology_tensors_fmi(raw_faces, tokens_per_face=9):
"""F + intra + inter 三通道拓扑分组(逐步共识用)。
F (refine 0/1/2): 按单父 coord 分组(每父冗余, 引用同父的面应一致)
intra(refine 3/4/5): 按单父 coord 分组(每父冗余)
inter(refine 6/7/8): 按"无序父对 coord"分组 —— 两个面若恰好共享 2 顶点(=共享一条粗边)
就共享同一父对, 其 inter 矩阵应一致; tokenizer 每面 3 父排序+按
(小,大)取 inter, 同一父对在任何面里 token 值相同(朝向天然规范)。
三通道用不同 key 前缀("F"/"I"/"O")互不表决。返回结构与 precompute_topology_tensors 完全一致,
可直接喂采样循环(各通道众数落各自值域, 不串)。
"""
B, num_faces, _, _ = raw_faces.shape
device = raw_faces.device
raw_faces_list = raw_faces.cpu().tolist()
single_ch = [(0, 0, "F"), (1, 1, "F"), (2, 2, "F"), (3, 0, "I"), (4, 1, "I"), (5, 2, "I")]
pair_ch = [(6, 0, 1), (7, 0, 2), (8, 1, 2)] # inter 位置 -> 父对
token_indices_list, v_indices_list, v_shared_count = [], [], 0
for b in range(B):
gd = {}
for f in range(num_faces):
fc = raw_faces_list[b][f]
base = b * (num_faces * tokens_per_face) + f * tokens_per_face
for pos, parent, tag in single_ch:
gd.setdefault((tag, tuple(fc[parent])), []).append(base + pos)
for pos, a, bp in pair_ch:
ca, cb = tuple(fc[a]), tuple(fc[bp])
pair = (ca, cb) if ca <= cb else (cb, ca)
gd.setdefault(("O", pair), []).append(base + pos)
for indices in gd.values():
if len(indices) > 1:
token_indices_list.extend(indices)
v_indices_list.extend([v_shared_count] * len(indices))
v_shared_count += 1
if len(token_indices_list) == 0:
return None, None, None, 0
token_idx = torch.tensor(token_indices_list, dtype=torch.long, device=device)
v_idx = torch.tensor(v_indices_list, dtype=torch.long, device=device)
face_idx = token_idx // tokens_per_face
return token_idx, v_idx, face_idx, v_shared_count