Add v7 distillation FP8 sparsity DINOv3 multiview symmetry MoE stack
Browse files- v7/README.md +59 -0
- v7/bench/__pycache__/quality_eval.cpython-311.pyc +0 -0
- v7/bench/quality_eval.py +17 -0
- v7/configs/l4_v7.yaml +116 -0
- v7/runtime/__pycache__/multiview.cpython-311.pyc +0 -0
- v7/runtime/__pycache__/symmetry.cpython-311.pyc +0 -0
- v7/runtime/__pycache__/timestep_cache.cpython-311.pyc +0 -0
- v7/runtime/multiview.py +22 -0
- v7/runtime/symmetry.py +51 -0
- v7/runtime/timestep_cache.py +33 -0
- v7/training/__pycache__/dinov3_bridge.cpython-311.pyc +0 -0
- v7/training/__pycache__/distill_core.cpython-311.pyc +0 -0
- v7/training/__pycache__/distill_dinov3.cpython-311.pyc +0 -0
- v7/training/__pycache__/distill_flow.cpython-311.pyc +0 -0
- v7/training/__pycache__/fp8_sparse.cpython-311.pyc +0 -0
- v7/training/__pycache__/geometry_moe.cpython-311.pyc +0 -0
- v7/training/__pycache__/lora.cpython-311.pyc +0 -0
- v7/training/__pycache__/teacher_hybrid.cpython-311.pyc +0 -0
- v7/training/dinov3_bridge.py +32 -0
- v7/training/distill_core.py +63 -0
- v7/training/distill_dinov3.py +36 -0
- v7/training/distill_flow.py +66 -0
- v7/training/fp8_sparse.py +51 -0
- v7/training/geometry_moe.py +39 -0
- v7/training/lora.py +40 -0
- v7/training/teacher_hybrid.py +47 -0
v7/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Companion Forge v7 — Distilled 3D Model Stack
|
| 2 |
+
|
| 3 |
+
Target: **Hugging Face Jobs `l4x1` / NVIDIA L4 (SM89, 24 GB VRAM)**.
|
| 4 |
+
|
| 5 |
+
v7 keeps the validated v6.4 ONNX/TensorRT runtime as the production fallback and adds a training/optimization layer inspired by the Kimi-K3 work: progressive few-step distillation, selective FP8, 2:4 structured sparsity, DINOv3 conditioning, multi-view fusion, geometry teachers, symmetry guidance, geometry-aware MoE, and timestep caching.
|
| 6 |
+
|
| 7 |
+
## Pipeline
|
| 8 |
+
|
| 9 |
+
```text
|
| 10 |
+
image/text
|
| 11 |
+
-> DINOv3 bridge + optional multi-view fusion
|
| 12 |
+
-> SS flow student (10 -> 4 -> 2 -> 1 steps)
|
| 13 |
+
-> SLat flow student (10 -> 4 -> 2 -> 1 steps)
|
| 14 |
+
-> full custom ONNX/TensorRT SLat DAE
|
| 15 |
+
-> MeshFlow/TRELLIS teacher losses during training only
|
| 16 |
+
-> AniGen skeleton/skin teacher
|
| 17 |
+
-> symmetry + rig guidance
|
| 18 |
+
-> rigged GLB
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
## What is implemented
|
| 22 |
+
|
| 23 |
+
- `training/distill_core.py`: progressive Euler macro-step KD + endpoint consistency + MeanFlow-style interval consistency.
|
| 24 |
+
- `training/distill_flow.py`: L4-friendly LoRA-then-merge training for SS/SLat flow students, stages `10→4→2→1`.
|
| 25 |
+
- `training/fp8_sparse.py`: selective ModelOpt FP8 hooks and exact magnitude-based 2:4 pruning for eligible Linear weights.
|
| 26 |
+
- `training/dinov3_bridge.py` + `distill_dinov3.py`: DINOv3 ViT-S bridge to AniGen's fixed `1374×1024` conditioning contract, distilled against DINOv2 ViT-L/14-reg.
|
| 27 |
+
- `runtime/multiview.py`: pose-Fourier multi-view token fusion for front/left/back/right conditioning.
|
| 28 |
+
- `runtime/symmetry.py`: flow-time reflection/C2/C4 velocity symmetrization plus rig symmetry loss.
|
| 29 |
+
- `training/teacher_hybrid.py`: cached MeshFlow/TRELLIS geometry teacher + AniGen rig teacher interface.
|
| 30 |
+
- `training/geometry_moe.py`: ModernMOE-inspired shared + top-k routed 3D experts.
|
| 31 |
+
- `runtime/timestep_cache.py`: residual timestep cache for >=4-step students; automatically unnecessary after 1–2-step distillation.
|
| 32 |
+
- `bench/quality_eval.py`: geometry/symmetry/joint/speed comparison harness.
|
| 33 |
+
|
| 34 |
+
## L4 training order
|
| 35 |
+
|
| 36 |
+
1. Distill DINOv3 bridge (keep current DINOv2 TRT runtime as fallback).
|
| 37 |
+
2. Build teacher cache using v6.4 outputs plus optional MeshFlow/TRELLIS geometry targets.
|
| 38 |
+
3. Distill SS `10→4`, SLat `10→4`; validate.
|
| 39 |
+
4. Apply selective FP8 PTQ; if quality drops, run short QAT.
|
| 40 |
+
5. Apply 2:4 pruning to MLP/projection weights, freeze masks, recovery distillation.
|
| 41 |
+
6. Distill `4→2`, then `2→1` only if geometry/rig metrics stay within thresholds.
|
| 42 |
+
7. Train geometry-inductive MoE as an optional higher-capacity student; distill it back to a dense/sparse deployment student if its routing overhead is not worthwhile on L4.
|
| 43 |
+
8. Re-export ONNX external-data and compile SM89 TensorRT engines. Output heads/mesh remain FP32; sparse topology ops remain the validated custom plugins.
|
| 44 |
+
|
| 45 |
+
## Safety/quality gates
|
| 46 |
+
|
| 47 |
+
A new student is not promoted merely because it is faster. Promotion requires finite tensors, exit code 0, GLB skin/skeleton validation, no lazy PyTorch fallback, and quality thresholds against the current v6.4 teacher. `1+1` is therefore an experimental final stage, not an automatic default.
|
| 48 |
+
|
| 49 |
+
## External technology mapping
|
| 50 |
+
|
| 51 |
+
- NVIDIA FastGen concepts: progressive KD / MeanFlow / consistency-style few-step training.
|
| 52 |
+
- NVIDIA Model Optimizer: FP8 PTQ/QAT and export path.
|
| 53 |
+
- TensorRT: 2:4 structured sparse tactics where eligible.
|
| 54 |
+
- Meta DINOv3: stronger dense visual conditioning.
|
| 55 |
+
- Meta MeshFlow / Microsoft TRELLIS.2: geometry/topology teacher targets only; licensing/runtime isolation is preserved.
|
| 56 |
+
- SymTRELLIS idea: velocity symmetrization adapted to AniGen flow and rig constraints.
|
| 57 |
+
- ModernMOE idea: routed/shared lightweight experts specialized for geometry roles.
|
| 58 |
+
|
| 59 |
+
The current v6.4 runtime remains the rollback target until each v7 stage has its own L4 E2E validation artifact.
|
v7/bench/__pycache__/quality_eval.cpython-311.pyc
ADDED
|
Binary file (4.15 kB). View file
|
|
|
v7/bench/quality_eval.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import argparse,json,time,torch
|
| 3 |
+
|
| 4 |
+
def chamfer(a,b):
|
| 5 |
+
d=torch.cdist(a.float()[None],b.float()[None]).squeeze(0);return float(d.min(1).values.mean()+d.min(0).values.mean())
|
| 6 |
+
def symmetry_x(v):
|
| 7 |
+
a=v.float().clone();a[:,0]*=-1;d=torch.cdist(v.float()[None],a[None]).squeeze(0);return float(d.min(1).values.mean())
|
| 8 |
+
def joint_error(a,b):return float(torch.linalg.vector_norm(a.float()-b.float(),dim=-1).mean()) if a.shape==b.shape else None
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
ap=argparse.ArgumentParser();ap.add_argument('--teacher',required=True);ap.add_argument('--student',required=True);ap.add_argument('--out',required=True);a=ap.parse_args();t=torch.load(a.teacher,map_location='cpu',weights_only=False);s=torch.load(a.student,map_location='cpu',weights_only=False)
|
| 12 |
+
r={}
|
| 13 |
+
if 'vertices' in t and 'vertices' in s:r['chamfer']=chamfer(s['vertices'],t['vertices']);r['symmetry_x']=symmetry_x(s['vertices'])
|
| 14 |
+
if 'joints' in t and 'joints' in s:r['joint_error']=joint_error(s['joints'],t['joints'])
|
| 15 |
+
if 'runtime_s' in s:r['runtime_s']=s['runtime_s'];r['speedup']=t.get('runtime_s',0)/s['runtime_s'] if t.get('runtime_s') else None
|
| 16 |
+
open(a.out,'w').write(json.dumps(r,indent=2));print(json.dumps(r))
|
| 17 |
+
if __name__=='__main__':main()
|
v7/configs/l4_v7.yaml
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: 7
|
| 2 |
+
target:
|
| 3 |
+
gpu: NVIDIA L4
|
| 4 |
+
compute_capability: 8.9
|
| 5 |
+
flavor: l4x1
|
| 6 |
+
vram_gib: 24
|
| 7 |
+
precision_default: fp16
|
| 8 |
+
tf32: true
|
| 9 |
+
|
| 10 |
+
distillation:
|
| 11 |
+
enabled: true
|
| 12 |
+
methods: [progressive_kd, meanflow, consistency]
|
| 13 |
+
stages:
|
| 14 |
+
- {teacher_steps: 10, student_steps: 4, train_steps: 6000}
|
| 15 |
+
- {teacher_steps: 4, student_steps: 2, train_steps: 8000}
|
| 16 |
+
- {teacher_steps: 2, student_steps: 1, train_steps: 12000}
|
| 17 |
+
components: [ss_flow, slat_flow]
|
| 18 |
+
train_mode: lora_then_merge
|
| 19 |
+
lora:
|
| 20 |
+
rank: 16
|
| 21 |
+
alpha: 32
|
| 22 |
+
dropout: 0.0
|
| 23 |
+
target_regex: '(qkv|to_q|to_k|to_v|proj|fc1|fc2|mlp|linear)'
|
| 24 |
+
optimizer: {name: adamw8bit, lr: 0.0001, weight_decay: 0.01}
|
| 25 |
+
gradient_checkpointing: true
|
| 26 |
+
grad_accum: 8
|
| 27 |
+
losses:
|
| 28 |
+
velocity_kd: 1.0
|
| 29 |
+
endpoint_consistency: 0.5
|
| 30 |
+
geometry_consistency: 0.25
|
| 31 |
+
skeleton_consistency: 0.25
|
| 32 |
+
symmetry: 0.10
|
| 33 |
+
|
| 34 |
+
quantization:
|
| 35 |
+
modelopt: true
|
| 36 |
+
mode: fp8_selective
|
| 37 |
+
calibration_samples: 256
|
| 38 |
+
qat_steps: 1500
|
| 39 |
+
include_regex: '(qkv|to_q|to_k|to_v|proj|fc1|fc2|mlp|linear)'
|
| 40 |
+
exclude_regex: '(geo_head|skin_head|skl_head|out_layer|mesh|sparse|conv|norm)'
|
| 41 |
+
output_heads_fp32: true
|
| 42 |
+
sparse_ops_fp16: true
|
| 43 |
+
|
| 44 |
+
structured_sparsity:
|
| 45 |
+
enabled: true
|
| 46 |
+
pattern: '2:4'
|
| 47 |
+
include_regex: '(fc1|fc2|mlp|proj)'
|
| 48 |
+
exclude_regex: '(qkv|geo_head|skin_head|skl_head|out_layer|mesh|sparse|conv)'
|
| 49 |
+
recovery_steps: 2000
|
| 50 |
+
fixed_mask: true
|
| 51 |
+
tensorrt_sparse_weights: true
|
| 52 |
+
|
| 53 |
+
dinov3:
|
| 54 |
+
enabled: true
|
| 55 |
+
student_model: facebook/dinov3-vits16-pretrain-lvd1689m
|
| 56 |
+
teacher_model: dinov2_vitl14_reg
|
| 57 |
+
output_dim: 1024
|
| 58 |
+
target_tokens: 1374
|
| 59 |
+
input_size: 512
|
| 60 |
+
distill_steps: 5000
|
| 61 |
+
loss: {mse: 1.0, cosine: 0.5}
|
| 62 |
+
export: {onnx: true, tensorrt: true, fp8: true}
|
| 63 |
+
|
| 64 |
+
multiview:
|
| 65 |
+
enabled: true
|
| 66 |
+
views: [front, left, back, right]
|
| 67 |
+
aggregation: pose_fourier_attention
|
| 68 |
+
pose_dim: 64
|
| 69 |
+
fallback_single_view: true
|
| 70 |
+
|
| 71 |
+
teachers:
|
| 72 |
+
meshflow:
|
| 73 |
+
enabled: true
|
| 74 |
+
repo: facebook/meshflow
|
| 75 |
+
role: geometry_topology_teacher
|
| 76 |
+
license_gate: noncommercial_research
|
| 77 |
+
trellis2:
|
| 78 |
+
enabled: true
|
| 79 |
+
repo: microsoft/TRELLIS.2-4B
|
| 80 |
+
role: pbr_geometry_teacher
|
| 81 |
+
anigen:
|
| 82 |
+
enabled: true
|
| 83 |
+
role: skeleton_skin_teacher
|
| 84 |
+
|
| 85 |
+
symmetry:
|
| 86 |
+
enabled: true
|
| 87 |
+
mode: velocity_symmetrization
|
| 88 |
+
groups: [reflection_x, c2, c4]
|
| 89 |
+
auto_detect: true
|
| 90 |
+
rig_aware:
|
| 91 |
+
bilateral_limbs: true
|
| 92 |
+
limb_length_penalty: 0.15
|
| 93 |
+
joint_mirror_penalty: 0.25
|
| 94 |
+
center_of_mass_penalty: 0.05
|
| 95 |
+
|
| 96 |
+
moe:
|
| 97 |
+
enabled: true
|
| 98 |
+
mode: geometry_inductive
|
| 99 |
+
router_top_k: 2
|
| 100 |
+
shared_experts: 1
|
| 101 |
+
experts: [coarse_shape, surface_detail, limbs, head_face, skeleton, skin, materials]
|
| 102 |
+
expert_hidden_ratio: 0.5
|
| 103 |
+
residual_reuse: true
|
| 104 |
+
train_after_few_step: true
|
| 105 |
+
|
| 106 |
+
cache_diffusion:
|
| 107 |
+
enabled: true
|
| 108 |
+
mode: residual_timestep_cache
|
| 109 |
+
apply_when_steps_ge: 4
|
| 110 |
+
similarity_threshold: 0.985
|
| 111 |
+
max_reuse: 2
|
| 112 |
+
|
| 113 |
+
artifacts:
|
| 114 |
+
runtime_repo: patdev/Companion-Forge-L4-ONNX
|
| 115 |
+
students_repo: patdev/Companion-Forge-v7-Students
|
| 116 |
+
teacher_cache_repo: patdev/Companion-Forge-v7-Teacher-Cache
|
v7/runtime/__pycache__/multiview.cpython-311.pyc
ADDED
|
Binary file (5.14 kB). View file
|
|
|
v7/runtime/__pycache__/symmetry.cpython-311.pyc
ADDED
|
Binary file (5.87 kB). View file
|
|
|
v7/runtime/__pycache__/timestep_cache.cpython-311.pyc
ADDED
|
Binary file (4.48 kB). View file
|
|
|
v7/runtime/multiview.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import math,torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
|
| 5 |
+
class PoseFourier(nn.Module):
|
| 6 |
+
def __init__(self,bands=8,out=64):super().__init__();self.bands=bands;self.proj=nn.Linear(bands*4,out)
|
| 7 |
+
def forward(self,azimuth_deg,elevation_deg):
|
| 8 |
+
a=torch.deg2rad(azimuth_deg.float());e=torch.deg2rad(elevation_deg.float());freq=2**torch.arange(self.bands,device=a.device,dtype=a.dtype)
|
| 9 |
+
z=torch.cat([torch.sin(a[:,None]*freq),torch.cos(a[:,None]*freq),torch.sin(e[:,None]*freq),torch.cos(e[:,None]*freq)],-1);return self.proj(z)
|
| 10 |
+
|
| 11 |
+
class MultiViewAggregator(nn.Module):
|
| 12 |
+
"""Order-aware but permutation-stable view fusion for AniGen condition tokens."""
|
| 13 |
+
def __init__(self,dim=1024,pose_dim=64,heads=8):
|
| 14 |
+
super().__init__();self.pose=PoseFourier(out=pose_dim);self.pose_proj=nn.Linear(pose_dim,dim);self.attn=nn.MultiheadAttention(dim,heads,batch_first=True);self.norm=nn.LayerNorm(dim)
|
| 15 |
+
def forward(self,view_tokens,azimuth,elevation,view_mask=None):
|
| 16 |
+
# view_tokens [B,V,T,C]
|
| 17 |
+
b,v,t,c=view_tokens.shape;p=self.pose(azimuth.reshape(-1),elevation.reshape(-1)).view(b,v,-1);x=view_tokens+self.pose_proj(p)[:,:,None,:]
|
| 18 |
+
# fuse each token position across views; output remains [B,T,C]
|
| 19 |
+
q=x.mean(1);kv=x.transpose(1,2).reshape(b*t,v,c);qq=q.reshape(b*t,1,c);key_padding=None if view_mask is None else ~view_mask[:,None,:].expand(b,t,v).reshape(b*t,v)
|
| 20 |
+
y,_=self.attn(qq,kv,kv,key_padding_mask=key_padding,need_weights=False);return self.norm(q+y.reshape(b,t,c))
|
| 21 |
+
|
| 22 |
+
DEFAULT_VIEWS={'front':(0.,0.),'left':(-90.,0.),'back':(180.,0.),'right':(90.,0.)}
|
v7/runtime/symmetry.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
# Flow-time symmetry projection inspired by velocity symmetrization: transform state,
|
| 5 |
+
# evaluate equivariant velocity externally, inverse-transform velocity, then average.
|
| 6 |
+
|
| 7 |
+
def dense_reflect_x(x): return torch.flip(x,[-1])
|
| 8 |
+
def dense_rot_z_90(x,k=1): return torch.rot90(x,k,(-2,-1))
|
| 9 |
+
|
| 10 |
+
def sparse_reflect_x(s,res=64):
|
| 11 |
+
c=s.coords.clone();c[:,3]=(res-1)-c[:,3];return s.replace(coords=c)
|
| 12 |
+
def sparse_rot_z_90(s,res=64,k=1):
|
| 13 |
+
c=s.coords.clone();x,y=c[:,3].clone(),c[:,2].clone();k%=4
|
| 14 |
+
if k==1:c[:,3],c[:,2]=(res-1)-y,x
|
| 15 |
+
elif k==2:c[:,3],c[:,2]=(res-1)-x,(res-1)-y
|
| 16 |
+
elif k==3:c[:,3],c[:,2]=y,(res-1)-x
|
| 17 |
+
return s.replace(coords=c)
|
| 18 |
+
|
| 19 |
+
def transform_state(x,kind,res=64,inverse=False):
|
| 20 |
+
sparse=hasattr(x,'coords') and hasattr(x,'replace')
|
| 21 |
+
if kind=='identity':return x
|
| 22 |
+
if kind=='reflection_x':return sparse_reflect_x(x,res) if sparse else dense_reflect_x(x)
|
| 23 |
+
if kind.startswith('rot_z_'):
|
| 24 |
+
k=int(kind.rsplit('_',1)[1]);k=(-k)%4 if inverse else k
|
| 25 |
+
return sparse_rot_z_90(x,res,k) if sparse else dense_rot_z_90(x,k)
|
| 26 |
+
raise ValueError(kind)
|
| 27 |
+
|
| 28 |
+
def group_ops(group):
|
| 29 |
+
if group=='reflection_x':return ['identity','reflection_x']
|
| 30 |
+
if group=='c2':return ['identity','rot_z_2']
|
| 31 |
+
if group=='c4':return ['identity','rot_z_1','rot_z_2','rot_z_3']
|
| 32 |
+
return ['identity']
|
| 33 |
+
|
| 34 |
+
@torch.no_grad()
|
| 35 |
+
def symmetrized_velocity(model,x,xs,t,cond,group='reflection_x',res=64,**kwargs):
|
| 36 |
+
vs=[];vss=[]
|
| 37 |
+
for op in group_ops(group):
|
| 38 |
+
tx=transform_state(x,op,res);ts=transform_state(xs,op,res);v,sv=model(tx,ts,t,cond,**kwargs)
|
| 39 |
+
inv=op
|
| 40 |
+
if op.startswith('rot_z_'):inv='rot_z_'+str((-int(op.rsplit('_',1)[1]))%4)
|
| 41 |
+
vs.append(transform_state(v,inv,res));vss.append(transform_state(sv,inv,res))
|
| 42 |
+
def avg(items):
|
| 43 |
+
if hasattr(items[0],'feats'):return items[0].replace(feats=torch.stack([z.feats for z in items]).mean(0))
|
| 44 |
+
return torch.stack(items).mean(0)
|
| 45 |
+
return avg(vs),avg(vss)
|
| 46 |
+
|
| 47 |
+
def rig_symmetry_loss(joints,parents=None):
|
| 48 |
+
"""Soft bilateral prior; assumes x axis is left/right in normalized object coordinates."""
|
| 49 |
+
if joints.numel()==0:return joints.sum()*0
|
| 50 |
+
x=joints[...,0];center=x.mean(-1,keepdim=True);sorted_x=torch.sort(x-center,dim=-1).values
|
| 51 |
+
mirror=-torch.flip(sorted_x,[-1]);return torch.nn.functional.smooth_l1_loss(sorted_x,mirror)
|
v7/runtime/timestep_cache.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
|
| 5 |
+
class ResidualTimestepCache:
|
| 6 |
+
"""Training-free residual cache for multi-step flow inference.
|
| 7 |
+
Stores model residual/velocity and reuses it only when normalized state similarity is high.
|
| 8 |
+
Intended for >=4-step students; disabled automatically for 1-2 step students.
|
| 9 |
+
"""
|
| 10 |
+
def __init__(self,threshold=.985,max_reuse=2):self.threshold=threshold;self.max_reuse=max_reuse;self.reset()
|
| 11 |
+
def reset(self):self.prev_state=None;self.prev_output=None;self.reuse_count=0
|
| 12 |
+
@staticmethod
|
| 13 |
+
def _feat(x):return x.feats if hasattr(x,'feats') else x
|
| 14 |
+
def similarity(self,x):
|
| 15 |
+
if self.prev_state is None:return -1.
|
| 16 |
+
a=self._feat(x).float().flatten(1);b=self._feat(self.prev_state).float().flatten(1)
|
| 17 |
+
if a.shape!=b.shape:return -1.
|
| 18 |
+
return float(F.cosine_similarity(a,b,dim=-1).mean())
|
| 19 |
+
def get(self,x):
|
| 20 |
+
sim=self.similarity(x)
|
| 21 |
+
if sim>=self.threshold and self.reuse_count<self.max_reuse:
|
| 22 |
+
self.reuse_count+=1;return self.prev_output,sim
|
| 23 |
+
return None,sim
|
| 24 |
+
def put(self,x,out):self.prev_state=x;self.prev_output=out;self.reuse_count=0
|
| 25 |
+
|
| 26 |
+
class CachedFlowModel:
|
| 27 |
+
def __init__(self,model,threshold=.985,max_reuse=2):self.model=model;self.cache=ResidualTimestepCache(threshold,max_reuse);self.hits=0;self.calls=0
|
| 28 |
+
def __call__(self,x,xs,t,cond,*args,**kwargs):
|
| 29 |
+
self.calls+=1;cached,sim=self.cache.get(x)
|
| 30 |
+
if cached is not None:self.hits+=1;return cached
|
| 31 |
+
out=self.model(x,xs,t,cond,*args,**kwargs);self.cache.put(x,out);return out
|
| 32 |
+
@property
|
| 33 |
+
def hit_rate(self):return self.hits/max(1,self.calls)
|
v7/training/__pycache__/dinov3_bridge.cpython-311.pyc
ADDED
|
Binary file (5.47 kB). View file
|
|
|
v7/training/__pycache__/distill_core.cpython-311.pyc
ADDED
|
Binary file (8.68 kB). View file
|
|
|
v7/training/__pycache__/distill_dinov3.cpython-311.pyc
ADDED
|
Binary file (7.62 kB). View file
|
|
|
v7/training/__pycache__/distill_flow.cpython-311.pyc
ADDED
|
Binary file (12.9 kB). View file
|
|
|
v7/training/__pycache__/fp8_sparse.cpython-311.pyc
ADDED
|
Binary file (7.31 kB). View file
|
|
|
v7/training/__pycache__/geometry_moe.cpython-311.pyc
ADDED
|
Binary file (8.1 kB). View file
|
|
|
v7/training/__pycache__/lora.cpython-311.pyc
ADDED
|
Binary file (5.52 kB). View file
|
|
|
v7/training/__pycache__/teacher_hybrid.cpython-311.pyc
ADDED
|
Binary file (5.59 kB). View file
|
|
|
v7/training/dinov3_bridge.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import math
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from transformers import AutoModel
|
| 7 |
+
|
| 8 |
+
class DINOv3ConditionBridge(nn.Module):
|
| 9 |
+
"""DINOv3 -> AniGen conditioning contract: 1374 tokens x 1024 dims.
|
| 10 |
+
1369 spatial tokens are obtained by 32x32 -> 37x37 interpolation; 5 special tokens
|
| 11 |
+
are learned from global features so the downstream AniGen cross-attention shape is unchanged.
|
| 12 |
+
"""
|
| 13 |
+
def __init__(self,model_id='facebook/dinov3-vits16-pretrain-lvd1689m',out_dim=1024,target_grid=37,special_tokens=5):
|
| 14 |
+
super().__init__();self.encoder=AutoModel.from_pretrained(model_id);h=self.encoder.config.hidden_size
|
| 15 |
+
self.proj=nn.Linear(h,out_dim,bias=False);self.special=nn.Sequential(nn.Linear(h,out_dim),nn.GELU(),nn.Linear(out_dim,special_tokens*out_dim));self.target_grid=target_grid;self.special_tokens=special_tokens;self.out_dim=out_dim
|
| 16 |
+
def forward(self,pixel_values):
|
| 17 |
+
o=self.encoder(pixel_values=pixel_values,return_dict=True);x=o.last_hidden_state
|
| 18 |
+
# DINOv3 HF exposes sequence tokens. Keep CLS/global separate and infer square patch tail.
|
| 19 |
+
g=x[:,0]; patches=x[:,1:];n=patches.shape[1];side=int(math.sqrt(n))
|
| 20 |
+
if side*side!=n:
|
| 21 |
+
# Some checkpoints expose register tokens; peel them from the front until the tail is square.
|
| 22 |
+
for r in range(min(16,n)):
|
| 23 |
+
q=n-r;s=int(math.sqrt(q))
|
| 24 |
+
if s*s==q: patches=patches[:,1+r:];side=s;break
|
| 25 |
+
b,c=patches.shape[0],patches.shape[-1];p=patches.transpose(1,2).reshape(b,c,side,side)
|
| 26 |
+
p=F.interpolate(p,size=(self.target_grid,self.target_grid),mode='bicubic',align_corners=False).flatten(2).transpose(1,2)
|
| 27 |
+
p=self.proj(p);sp=self.special(g).view(b,self.special_tokens,self.out_dim);return torch.cat([sp,p],dim=1)
|
| 28 |
+
|
| 29 |
+
class FeatureDistillLoss(nn.Module):
|
| 30 |
+
def __init__(self,mse=1.0,cosine=0.5):super().__init__();self.mse=mse;self.cosine=cosine
|
| 31 |
+
def forward(self,student,teacher):
|
| 32 |
+
a=F.mse_loss(student.float(),teacher.float());b=1-F.cosine_similarity(student.float(),teacher.float(),dim=-1).mean();return self.mse*a+self.cosine*b,{'mse':a.detach(),'cosine_loss':b.detach()}
|
v7/training/distill_core.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import Any, Callable
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class FlowBatch:
|
| 9 |
+
x: Any
|
| 10 |
+
x_skl: Any
|
| 11 |
+
cond: Any
|
| 12 |
+
neg_cond: Any|None=None
|
| 13 |
+
kwargs: dict|None=None
|
| 14 |
+
|
| 15 |
+
class StateAdapter:
|
| 16 |
+
"""Math on dense tensors or AniGen SparseTensor-like objects (.feats/.replace)."""
|
| 17 |
+
@staticmethod
|
| 18 |
+
def feats(x): return x.feats if hasattr(x,'feats') else x
|
| 19 |
+
@staticmethod
|
| 20 |
+
def replace(x, feats): return x.replace(feats=feats) if hasattr(x,'replace') else feats
|
| 21 |
+
@classmethod
|
| 22 |
+
def add(cls,a,b,alpha=1.0): return cls.replace(a,cls.feats(a)+alpha*cls.feats(b))
|
| 23 |
+
@classmethod
|
| 24 |
+
def sub(cls,a,b): return cls.replace(a,cls.feats(a)-cls.feats(b))
|
| 25 |
+
@classmethod
|
| 26 |
+
def scale(cls,a,s): return cls.replace(a,cls.feats(a)*s)
|
| 27 |
+
@classmethod
|
| 28 |
+
def mse(cls,a,b): return F.mse_loss(cls.feats(a).float(),cls.feats(b).float())
|
| 29 |
+
|
| 30 |
+
@torch.no_grad()
|
| 31 |
+
def teacher_rollout(model, batch:FlowBatch, t_hi:float, t_lo:float, steps:int):
|
| 32 |
+
"""Euler teacher trajectory from noise-side t_hi toward data-side t_lo."""
|
| 33 |
+
assert t_hi>t_lo and steps>=1
|
| 34 |
+
x,xs=batch.x,batch.x_skl; dt=(t_hi-t_lo)/steps; kw=batch.kwargs or {}
|
| 35 |
+
t=t_hi
|
| 36 |
+
for _ in range(steps):
|
| 37 |
+
tt=torch.full((StateAdapter.feats(x).shape[0],),t*1000,device=StateAdapter.feats(x).device,dtype=torch.float32)
|
| 38 |
+
v,vs=model(x,xs,tt,batch.cond,neg_cond=batch.neg_cond,**kw)
|
| 39 |
+
x=StateAdapter.add(x,v,alpha=-dt); xs=StateAdapter.add(xs,vs,alpha=-dt); t-=dt
|
| 40 |
+
return x,xs
|
| 41 |
+
|
| 42 |
+
def macro_velocity(x_start,x_end,delta_t:float): return StateAdapter.scale(StateAdapter.sub(x_start,x_end),1.0/delta_t)
|
| 43 |
+
|
| 44 |
+
def student_loss(student,batch:FlowBatch,t_hi:float,t_lo:float,teacher_end,weights:dict):
|
| 45 |
+
dt=t_hi-t_lo; x_end,xs_end=teacher_end; kw=batch.kwargs or {}
|
| 46 |
+
tt=torch.full((StateAdapter.feats(batch.x).shape[0],),t_hi*1000,device=StateAdapter.feats(batch.x).device,dtype=torch.float32)
|
| 47 |
+
pv,pvs=student(batch.x,batch.x_skl,tt,batch.cond,neg_cond=batch.neg_cond,**kw)
|
| 48 |
+
tv=macro_velocity(batch.x,x_end,dt); tvs=macro_velocity(batch.x_skl,xs_end,dt)
|
| 49 |
+
vel=StateAdapter.mse(pv,tv)+StateAdapter.mse(pvs,tvs)
|
| 50 |
+
pred_end=StateAdapter.add(batch.x,pv,alpha=-dt); pred_skl_end=StateAdapter.add(batch.x_skl,pvs,alpha=-dt)
|
| 51 |
+
endpoint=StateAdapter.mse(pred_end,x_end)+StateAdapter.mse(pred_skl_end,xs_end)
|
| 52 |
+
return weights.get('velocity_kd',1.0)*vel+weights.get('endpoint_consistency',0.5)*endpoint, {'velocity':float(vel.detach()),'endpoint':float(endpoint.detach())}
|
| 53 |
+
|
| 54 |
+
def meanflow_pair_loss(student,batch:FlowBatch,t0:float,t1:float,teacher_end,weight:float=0.25):
|
| 55 |
+
"""MeanFlow-style interval consistency: same macro displacement from both endpoints."""
|
| 56 |
+
if weight<=0:return torch.zeros((),device=StateAdapter.feats(batch.x).device)
|
| 57 |
+
x1,s1=teacher_end; mid=(t0+t1)/2; kw=batch.kwargs or {}; bsz=StateAdapter.feats(batch.x).shape[0]
|
| 58 |
+
tm=torch.full((bsz,),mid*1000,device=StateAdapter.feats(batch.x).device,dtype=torch.float32)
|
| 59 |
+
# Approximate midpoint state from teacher endpoints; stable and cheap for LoRA distillation.
|
| 60 |
+
xm=StateAdapter.scale(StateAdapter.add(batch.x,x1),0.5); sm=StateAdapter.scale(StateAdapter.add(batch.x_skl,s1),0.5)
|
| 61 |
+
vm,vsm=student(xm,sm,tm,batch.cond,neg_cond=batch.neg_cond,**kw)
|
| 62 |
+
target=macro_velocity(batch.x,x1,t0-t1); target_s=macro_velocity(batch.x_skl,s1,t0-t1)
|
| 63 |
+
return weight*(StateAdapter.mse(vm,target)+StateAdapter.mse(vsm,target_s))
|
v7/training/distill_dinov3.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import argparse,os,sys,json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np,torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from torchvision import transforms
|
| 8 |
+
from .dinov3_bridge import DINOv3ConditionBridge,FeatureDistillLoss
|
| 9 |
+
|
| 10 |
+
MEAN=(.485,.456,.406);STD=(.229,.224,.225)
|
| 11 |
+
def image_tensor(p,size,device):
|
| 12 |
+
im=Image.open(p).convert('RGB').resize((size,size),Image.LANCZOS);x=torch.from_numpy(np.asarray(im).copy()).permute(2,0,1).float()/255.;x=transforms.Normalize(MEAN,STD)(x);return x[None].to(device)
|
| 13 |
+
|
| 14 |
+
def load_teacher(root='/home/user/app'):
|
| 15 |
+
sys.path.insert(0,root);os.chdir(root);m=torch.hub.load('./ckpts/dinov2','dinov2_vitl14_reg',pretrained=True,source='local').eval().cuda();
|
| 16 |
+
for p in m.parameters():p.requires_grad=False
|
| 17 |
+
return m
|
| 18 |
+
|
| 19 |
+
@torch.no_grad()
|
| 20 |
+
def teacher_features(m,x):
|
| 21 |
+
f=m(x,is_training=True)['x_prenorm'];return F.layer_norm(f,f.shape[-1:])
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
ap=argparse.ArgumentParser();ap.add_argument('--images',required=True);ap.add_argument('--out',required=True);ap.add_argument('--model',default='facebook/dinov3-vits16-pretrain-lvd1689m');ap.add_argument('--steps',type=int,default=5000);ap.add_argument('--lr',type=float,default=2e-4);a=ap.parse_args()
|
| 25 |
+
files=[p for p in Path(a.images).rglob('*') if p.suffix.lower() in {'.png','.jpg','.jpeg','.webp'}];assert files
|
| 26 |
+
teacher=load_teacher();student=DINOv3ConditionBridge(a.model).cuda().train()
|
| 27 |
+
for p in student.encoder.parameters():p.requires_grad=False
|
| 28 |
+
opt=torch.optim.AdamW([*student.proj.parameters(),*student.special.parameters()],lr=a.lr,weight_decay=.01);crit=FeatureDistillLoss(1,.5)
|
| 29 |
+
for step in range(1,a.steps+1):
|
| 30 |
+
p=files[(step-1)%len(files)];xt=image_tensor(p,518,'cuda');xs=image_tensor(p,512,'cuda')
|
| 31 |
+
with torch.inference_mode(),torch.autocast('cuda',dtype=torch.float16):yt=teacher_features(teacher,xt)
|
| 32 |
+
with torch.autocast('cuda',dtype=torch.float16):ys=student(xs);loss,parts=crit(ys,yt);loss.backward()
|
| 33 |
+
opt.step();opt.zero_grad(set_to_none=True)
|
| 34 |
+
if step==1 or step%100==0:print(json.dumps({'step':step,'loss':float(loss.detach()),'mse':float(parts['mse']),'cosine_loss':float(parts['cosine_loss'])}),flush=True)
|
| 35 |
+
out=Path(a.out);out.mkdir(parents=True,exist_ok=True);torch.save({'model':student.state_dict(),'model_id':a.model,'target_tokens':1374,'output_dim':1024},out/'dinov3_bridge.pt')
|
| 36 |
+
if __name__=='__main__':main()
|
v7/training/distill_flow.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import argparse,copy,json,os,random,sys,time
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import torch,yaml
|
| 5 |
+
from torch.utils.data import Dataset,DataLoader
|
| 6 |
+
from .lora import inject_lora,trainable_parameters,merge_lora
|
| 7 |
+
from .distill_core import FlowBatch,teacher_rollout,student_loss,meanflow_pair_loss,StateAdapter
|
| 8 |
+
|
| 9 |
+
class CacheDataset(Dataset):
|
| 10 |
+
def __init__(self,root): self.files=sorted(Path(root).glob('*.pt')); assert self.files,f'No .pt cache files in {root}'
|
| 11 |
+
def __len__(self): return len(self.files)
|
| 12 |
+
def __getitem__(self,i): return torch.load(self.files[i],map_location='cpu',weights_only=False)
|
| 13 |
+
|
| 14 |
+
def move(v,dev):
|
| 15 |
+
if torch.is_tensor(v): return v.to(dev,non_blocking=True)
|
| 16 |
+
if isinstance(v,dict): return {k:move(x,dev) for k,x in v.items()}
|
| 17 |
+
if isinstance(v,(list,tuple)): return type(v)(move(x,dev) for x in v)
|
| 18 |
+
return v
|
| 19 |
+
|
| 20 |
+
def make_batch(d,dev):
|
| 21 |
+
return FlowBatch(move(d['x'],dev),move(d['x_skl'],dev),move(d['cond'],dev),move(d.get('neg_cond'),dev),move(d.get('kwargs',{}),dev))
|
| 22 |
+
|
| 23 |
+
def load_anigen_model(component,root='/home/user/app'):
|
| 24 |
+
sys.path.insert(0,root); os.chdir(root)
|
| 25 |
+
from anigen.utils.model_utils import load_model_from_path
|
| 26 |
+
path='ckpts/anigen/ss_flow_solo' if component=='ss_flow' else 'ckpts/anigen/slat_flow_auto'
|
| 27 |
+
model,cfg=load_model_from_path(path,model_name_in_config='denoiser',device='cuda',use_ema=False)
|
| 28 |
+
return model,cfg
|
| 29 |
+
|
| 30 |
+
def save_state(model,out,meta):
|
| 31 |
+
out=Path(out);out.mkdir(parents=True,exist_ok=True)
|
| 32 |
+
torch.save(model.state_dict(),out/'student_merged.pt');(out/'meta.json').write_text(json.dumps(meta,indent=2))
|
| 33 |
+
|
| 34 |
+
def train_stage(component,cfg,stage,cache,out,resume=None):
|
| 35 |
+
torch.backends.cuda.matmul.allow_tf32=True; torch.backends.cudnn.allow_tf32=True
|
| 36 |
+
teacher,_=load_anigen_model(component); teacher.eval();
|
| 37 |
+
for p in teacher.parameters(): p.requires_grad=False
|
| 38 |
+
student=copy.deepcopy(teacher).train(); replaced=inject_lora(student,cfg['distillation']['lora']['target_regex'],cfg['distillation']['lora']['rank'],cfg['distillation']['lora']['alpha'],cfg['distillation']['lora'].get('dropout',0))
|
| 39 |
+
if resume: student.load_state_dict(torch.load(resume,map_location='cpu',weights_only=False),strict=False)
|
| 40 |
+
params=trainable_parameters(student); opt=torch.optim.AdamW(params,lr=cfg['distillation']['optimizer']['lr'],weight_decay=cfg['distillation']['optimizer']['weight_decay'])
|
| 41 |
+
ds=CacheDataset(cache); dl=DataLoader(ds,batch_size=1,shuffle=True,num_workers=2,pin_memory=True,collate_fn=lambda x:x[0]);it=iter(dl)
|
| 42 |
+
accum=int(cfg['distillation'].get('grad_accum',8)); total=int(stage['train_steps']); t0=time.time(); opt.zero_grad(set_to_none=True)
|
| 43 |
+
for step in range(1,total+1):
|
| 44 |
+
try:d=next(it)
|
| 45 |
+
except StopIteration:it=iter(dl);d=next(it)
|
| 46 |
+
b=make_batch(d,'cuda'); # cached states may specify interval; otherwise sample macro interval.
|
| 47 |
+
t_hi=float(d.get('t_hi',random.uniform(0.25,1.0))); width=1.0/int(stage['student_steps']); t_lo=max(0.0,t_hi-width)
|
| 48 |
+
# Teacher uses the previous-stage budget over this macro interval.
|
| 49 |
+
local_teacher_steps=max(1,round(stage['teacher_steps']/stage['student_steps']))
|
| 50 |
+
with torch.inference_mode(),torch.autocast('cuda',dtype=torch.float16): te=teacher_rollout(teacher,b,t_hi,t_lo,local_teacher_steps)
|
| 51 |
+
with torch.autocast('cuda',dtype=torch.float16):
|
| 52 |
+
loss,parts=student_loss(student,b,t_hi,t_lo,te,cfg['distillation']['losses'])
|
| 53 |
+
loss=loss+meanflow_pair_loss(student,b,t_hi,t_lo,te,0.25 if 'meanflow' in cfg['distillation']['methods'] else 0.0)
|
| 54 |
+
(loss/accum).backward()
|
| 55 |
+
if step%accum==0:
|
| 56 |
+
torch.nn.utils.clip_grad_norm_(params,1.0);opt.step();opt.zero_grad(set_to_none=True)
|
| 57 |
+
if step==1 or step%100==0: print(json.dumps({'step':step,'loss':float(loss.detach()),**parts,'elapsed_s':round(time.time()-t0,1)}),flush=True)
|
| 58 |
+
if step%1000==0: torch.save(student.state_dict(),Path(out)/f'adapter_step_{step}.pt')
|
| 59 |
+
merged=merge_lora(student); meta={'component':component,'stage':stage,'lora_modules':replaced,'merged_modules':merged,'method':cfg['distillation']['methods'],'base':'VAST-AI/AniGen'};save_state(student,out,meta)
|
| 60 |
+
|
| 61 |
+
def main():
|
| 62 |
+
ap=argparse.ArgumentParser();ap.add_argument('--config',default='v7/configs/l4_v7.yaml');ap.add_argument('--component',choices=['ss_flow','slat_flow'],required=True);ap.add_argument('--stage',type=int,default=0);ap.add_argument('--cache',required=True);ap.add_argument('--out',required=True);ap.add_argument('--resume');ap.add_argument('--steps',type=int);a=ap.parse_args()
|
| 63 |
+
cfg=yaml.safe_load(open(a.config)); stage=dict(cfg['distillation']['stages'][a.stage]);
|
| 64 |
+
if a.steps:stage['train_steps']=a.steps
|
| 65 |
+
train_stage(a.component,cfg,stage,a.cache,a.out,a.resume)
|
| 66 |
+
if __name__=='__main__':main()
|
v7/training/fp8_sparse.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import argparse,json,re,torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
|
| 5 |
+
@torch.no_grad()
|
| 6 |
+
def prune_2to4_(model:nn.Module,include:str,exclude:str):
|
| 7 |
+
inc,rex=re.compile(include),re.compile(exclude); touched=[]
|
| 8 |
+
for name,m in model.named_modules():
|
| 9 |
+
if not isinstance(m,nn.Linear) or not inc.search(name) or rex.search(name): continue
|
| 10 |
+
w=m.weight.data; k=w.shape[-1]
|
| 11 |
+
if k%4: continue
|
| 12 |
+
flat=w.reshape(-1,k); g=flat.reshape(-1,k//4,4); idx=g.abs().argsort(dim=-1)[...,:2]
|
| 13 |
+
mask=torch.ones_like(g,dtype=torch.bool); mask.scatter_(-1,idx,False); g.mul_(mask); touched.append(name)
|
| 14 |
+
return touched
|
| 15 |
+
|
| 16 |
+
def enforce_2to4_masks(model,masks):
|
| 17 |
+
with torch.no_grad():
|
| 18 |
+
for n,p in model.named_parameters():
|
| 19 |
+
if n in masks:p.mul_(masks[n])
|
| 20 |
+
|
| 21 |
+
def capture_masks(model): return {n:(p!=0).to(p.dtype) for n,p in model.named_parameters() if p.ndim==2 and (p==0).any()}
|
| 22 |
+
|
| 23 |
+
def modelopt_fp8(model,forward_loop,include,exclude):
|
| 24 |
+
import modelopt.torch.quantization as mtq
|
| 25 |
+
from modelopt.torch.quantization.config import FP8_DEFAULT_CFG
|
| 26 |
+
cfg=dict(FP8_DEFAULT_CFG); q=list(cfg.get('quant_cfg',[])) if isinstance(cfg.get('quant_cfg'),list) else cfg.get('quant_cfg',{})
|
| 27 |
+
# ModelOpt supports name filters globally; keep sensitive geometry/output paths unquantized.
|
| 28 |
+
if isinstance(q,dict):
|
| 29 |
+
q[f'*{exclude}*']={'enable':False}
|
| 30 |
+
def calibrate(m): forward_loop(m)
|
| 31 |
+
mtq.quantize(model=model,config=cfg,forward_func=calibrate)
|
| 32 |
+
# Explicitly disable excluded module quantizers when available.
|
| 33 |
+
for name,module in model.named_modules():
|
| 34 |
+
if re.search(exclude,name):
|
| 35 |
+
for attr in ('input_quantizer','weight_quantizer','output_quantizer'):
|
| 36 |
+
qz=getattr(module,attr,None)
|
| 37 |
+
if qz is not None and hasattr(qz,'disable'): qz.disable()
|
| 38 |
+
return model
|
| 39 |
+
|
| 40 |
+
def selective_report(model,include,exclude):
|
| 41 |
+
rows=[]
|
| 42 |
+
for n,m in model.named_modules():
|
| 43 |
+
if isinstance(m,nn.Linear): rows.append({'name':n,'shape':list(m.weight.shape),'fp8_candidate':bool(re.search(include,n) and not re.search(exclude,n))})
|
| 44 |
+
return rows
|
| 45 |
+
|
| 46 |
+
def main():
|
| 47 |
+
ap=argparse.ArgumentParser();ap.add_argument('--model',required=True);ap.add_argument('--out',required=True);ap.add_argument('--include',default='(qkv|to_q|to_k|to_v|proj|fc1|fc2|mlp|linear)');ap.add_argument('--exclude',default='(geo_head|skin_head|skl_head|out_layer|mesh|sparse|conv|norm)');ap.add_argument('--sparsity',action='store_true');a=ap.parse_args()
|
| 48 |
+
model=torch.load(a.model,map_location='cpu',weights_only=False)
|
| 49 |
+
touched=prune_2to4_(model,a.include,a.exclude) if a.sparsity else []
|
| 50 |
+
torch.save(model,a.out);open(a.out+'.report.json','w').write(json.dumps({'sparse_modules':touched,'layers':selective_report(model,a.include,a.exclude)},indent=2))
|
| 51 |
+
if __name__=='__main__':main()
|
v7/training/geometry_moe.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
EXPERT_NAMES=('coarse_shape','surface_detail','limbs','head_face','skeleton','skin','materials')
|
| 7 |
+
|
| 8 |
+
class GeometryRouter(nn.Module):
|
| 9 |
+
def __init__(self,dim,num_experts=len(EXPERT_NAMES),top_k=2):super().__init__();self.top_k=top_k;self.norm=nn.LayerNorm(dim);self.gate=nn.Linear(dim,num_experts,bias=False)
|
| 10 |
+
def forward(self,x,context=None):
|
| 11 |
+
h=self.norm(x if context is None else x+context);logits=self.gate(h.float());v,i=torch.topk(logits,self.top_k,dim=-1);return i,F.softmax(v,dim=-1).to(x.dtype)
|
| 12 |
+
|
| 13 |
+
class ExpertMLP(nn.Module):
|
| 14 |
+
def __init__(self,dim,hidden,ratio=0.5):super().__init__();h=max(64,int(hidden*ratio));self.net=nn.Sequential(nn.Linear(dim,h),nn.SiLU(),nn.Linear(h,dim))
|
| 15 |
+
def forward(self,x):return self.net(x)
|
| 16 |
+
|
| 17 |
+
class GeometryInductiveMoE(nn.Module):
|
| 18 |
+
"""ModernMOE-inspired shared+routed experts for 3D flow transformer FFNs.
|
| 19 |
+
Expert semantics are encouraged by auxiliary labels/losses; routing itself remains learned.
|
| 20 |
+
"""
|
| 21 |
+
def __init__(self,dim,hidden,num_experts=len(EXPERT_NAMES),top_k=2,expert_ratio=.5):
|
| 22 |
+
super().__init__();self.names=EXPERT_NAMES[:num_experts];self.router=GeometryRouter(dim,num_experts,top_k);self.shared=ExpertMLP(dim,hidden,expert_ratio);self.experts=nn.ModuleList([ExpertMLP(dim,hidden,expert_ratio) for _ in range(num_experts)]);self.out_norm=nn.LayerNorm(dim)
|
| 23 |
+
def forward(self,x,context=None):
|
| 24 |
+
idx,w=self.router(x,context);out=self.shared(x)
|
| 25 |
+
# Top-k dispatch; optimized Triton/TRT plugin can replace this without changing weights.
|
| 26 |
+
for slot in range(idx.shape[-1]):
|
| 27 |
+
ids=idx[...,slot];ws=w[...,slot]
|
| 28 |
+
for eid,expert in enumerate(self.experts):
|
| 29 |
+
mask=ids.eq(eid)
|
| 30 |
+
if not mask.any():continue
|
| 31 |
+
y=expert(x[mask]);out[mask]=out[mask]+y*ws[mask,None]
|
| 32 |
+
return self.out_norm(out),{'expert_idx':idx,'expert_weight':w}
|
| 33 |
+
|
| 34 |
+
def load_balance_loss(router_info,num_experts):
|
| 35 |
+
idx=router_info['expert_idx'];w=router_info['expert_weight'];freq=torch.stack([(idx==i).float().mean() for i in range(num_experts)]);prob=torch.stack([torch.where(idx==i,w,torch.zeros_like(w)).sum(-1).mean() for i in range(num_experts)]);return num_experts*(freq*prob).sum()
|
| 36 |
+
|
| 37 |
+
def expert_hint_loss(router_info,hints):
|
| 38 |
+
"""hints: integer expert ids per token from geometry heuristics/teacher labels."""
|
| 39 |
+
idx=router_info['expert_idx'][...,0];return (idx!=hints).float().mean()
|
v7/training/lora.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import math,re
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
|
| 6 |
+
class LoRALinear(nn.Module):
|
| 7 |
+
def __init__(self, base: nn.Linear, rank: int=16, alpha: float=32.0, dropout: float=0.0):
|
| 8 |
+
super().__init__(); self.base=base; self.rank=rank; self.scale=alpha/rank
|
| 9 |
+
self.lora_a=nn.Linear(base.in_features,rank,bias=False,dtype=base.weight.dtype,device=base.weight.device)
|
| 10 |
+
self.lora_b=nn.Linear(rank,base.out_features,bias=False,dtype=base.weight.dtype,device=base.weight.device)
|
| 11 |
+
self.drop=nn.Dropout(dropout) if dropout else nn.Identity()
|
| 12 |
+
nn.init.kaiming_uniform_(self.lora_a.weight,a=math.sqrt(5)); nn.init.zeros_(self.lora_b.weight)
|
| 13 |
+
for p in self.base.parameters(): p.requires_grad=False
|
| 14 |
+
def forward(self,x): return self.base(x)+self.lora_b(self.lora_a(self.drop(x)))*self.scale
|
| 15 |
+
@torch.no_grad()
|
| 16 |
+
def merge(self):
|
| 17 |
+
delta=(self.lora_b.weight.float()@self.lora_a.weight.float())*self.scale
|
| 18 |
+
self.base.weight.add_(delta.to(self.base.weight.dtype)); return self.base
|
| 19 |
+
|
| 20 |
+
def inject_lora(model: nn.Module, pattern: str, rank=16, alpha=32.0, dropout=0.0):
|
| 21 |
+
rx=re.compile(pattern); replaced=[]
|
| 22 |
+
for full_name,module in list(model.named_modules()):
|
| 23 |
+
if not isinstance(module,nn.Linear) or not rx.search(full_name): continue
|
| 24 |
+
if '.' in full_name: parent_name,leaf=full_name.rsplit('.',1); parent=model.get_submodule(parent_name)
|
| 25 |
+
else: parent,leaf=model,full_name
|
| 26 |
+
setattr(parent,leaf,LoRALinear(module,rank,alpha,dropout)); replaced.append(full_name)
|
| 27 |
+
if not replaced: raise RuntimeError(f'No Linear matched LoRA regex: {pattern}')
|
| 28 |
+
return replaced
|
| 29 |
+
|
| 30 |
+
def trainable_parameters(model): return [p for p in model.parameters() if p.requires_grad]
|
| 31 |
+
|
| 32 |
+
def merge_lora(model: nn.Module):
|
| 33 |
+
merged=[]
|
| 34 |
+
for full_name,module in list(model.named_modules()):
|
| 35 |
+
if not isinstance(module,LoRALinear): continue
|
| 36 |
+
base=module.merge()
|
| 37 |
+
if '.' in full_name: parent_name,leaf=full_name.rsplit('.',1); parent=model.get_submodule(parent_name)
|
| 38 |
+
else: parent,leaf=model,full_name
|
| 39 |
+
setattr(parent,leaf,base); merged.append(full_name)
|
| 40 |
+
return merged
|
v7/training/teacher_hybrid.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import json,torch
|
| 5 |
+
|
| 6 |
+
@dataclass
|
| 7 |
+
class TeacherTarget:
|
| 8 |
+
vertices: torch.Tensor|None=None
|
| 9 |
+
faces: torch.Tensor|None=None
|
| 10 |
+
normals: torch.Tensor|None=None
|
| 11 |
+
joints: torch.Tensor|None=None
|
| 12 |
+
parents: torch.Tensor|None=None
|
| 13 |
+
skin: torch.Tensor|None=None
|
| 14 |
+
materials: dict|None=None
|
| 15 |
+
source: str=''
|
| 16 |
+
|
| 17 |
+
class TeacherEnsemble:
|
| 18 |
+
"""Geometry teacher = MeshFlow/TRELLIS.2; rig teacher = AniGen.
|
| 19 |
+
The adapters are intentionally lazy because MeshFlow has a non-commercial research license
|
| 20 |
+
and TRELLIS.2 has a distinct CUDA stack. Cache outputs once, then train students from tensors.
|
| 21 |
+
"""
|
| 22 |
+
def __init__(self,meshflow=None,trellis=None,anigen=None):self.meshflow=meshflow;self.trellis=trellis;self.anigen=anigen
|
| 23 |
+
@torch.no_grad()
|
| 24 |
+
def generate(self,image,prefer='meshflow'):
|
| 25 |
+
geom=None
|
| 26 |
+
if prefer=='meshflow' and self.meshflow is not None:geom=self.meshflow(image)
|
| 27 |
+
elif self.trellis is not None:geom=self.trellis(image)
|
| 28 |
+
rig=self.anigen(image) if self.anigen is not None else None
|
| 29 |
+
return {'geometry':geom,'rig':rig}
|
| 30 |
+
|
| 31 |
+
def chamfer_like(a,b,max_points=8192):
|
| 32 |
+
if a is None or b is None:return torch.tensor(0.,device=(a.device if a is not None else b.device))
|
| 33 |
+
if a.shape[0]>max_points:a=a[torch.randperm(a.shape[0],device=a.device)[:max_points]]
|
| 34 |
+
if b.shape[0]>max_points:b=b[torch.randperm(b.shape[0],device=b.device)[:max_points]]
|
| 35 |
+
d=torch.cdist(a.float()[None],b.float()[None]).squeeze(0);return d.min(1).values.mean()+d.min(0).values.mean()
|
| 36 |
+
|
| 37 |
+
def geometry_teacher_loss(student_vertices,target:TeacherTarget):
|
| 38 |
+
return chamfer_like(student_vertices,target.vertices) if target.vertices is not None else student_vertices.sum()*0
|
| 39 |
+
|
| 40 |
+
def rig_teacher_loss(student_joints,student_skin,target:TeacherTarget):
|
| 41 |
+
loss=student_joints.sum()*0
|
| 42 |
+
if target.joints is not None and student_joints.shape==target.joints.shape:loss=loss+torch.nn.functional.smooth_l1_loss(student_joints.float(),target.joints.float())
|
| 43 |
+
if target.skin is not None and student_skin.shape==target.skin.shape:loss=loss+torch.nn.functional.kl_div(student_skin.float().log_softmax(-1),target.skin.float().softmax(-1),reduction='batchmean')
|
| 44 |
+
return loss
|
| 45 |
+
|
| 46 |
+
def save_teacher_cache(path,record):
|
| 47 |
+
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);torch.save(record,path)
|