patdev commited on
Commit
27fe41c
·
verified ·
1 Parent(s): 47ac483

Add v7 distillation FP8 sparsity DINOv3 multiview symmetry MoE stack

Browse files
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)