复现实施方案 - VN02/Personal-profile GitHub Wiki
本文目标:给出一条可执行的 Kimodo 复现路线—— 把 NVIDIA 发布的 kimodo 推理包当作不可改动的「已验证模型库」直接 import,只在它之上加一层薄训练代码(dataset + loss + loop + config)。
这样做的决定性理由:kimodo 推理包里训练前向所需的模块(denoiser / motion_rep / diffusion / backbone / constraints / skeleton / stats)已经全齐,且本就是 training-ready 的(
twostage_denoiser.forward里带self.training分支)。NVIDIA 只是发布时摘掉了训练 harness。因此我们能做闭环验证:先用发布权重跑通推理、确认前向 100% 正确,再在这个「已验证常量」之上加训练层——训练代码错了能立刻定位。路线选型的完整论证见 §1。被否的两条路(改单文本扩散框架 / 到处复制拼装)也在 §1 说明。
事实分档:【源码】= 逐行读过 kimodo 推理代码/权重 config;【文档】= 仓库 README / benchmark docs;【报告】= Kimodo technical report 明确给出的训练配方/超参/消融。三档互为印证:报告给宏观配方,源码给精确格式,二者一致时即可照实现。
| 路线 | 做法 | 判定 |
|---|---|---|
| A. 改通用单文本扩散框架 | 在某个开源单阶段 text2motion 训练框架上把模型换成 Kimodo | ❌ 器官移植:单阶段 / smplrifke / CLIP / DDPM 模型本体全要扔,只剩训练外壳,却要一直迁就它的抽象 |
| B. 从各仓复制拼装 | 把 kimodo 的 motion_rep/denoiser 手抄进新工程 | ❌ 致命:手抄会引入通道顺序/归一化/6D 约定/detach 位置的微妙偏差,且无法与发布权重对齐验证 |
| C. 宿主 + 薄训练层 |
import kimodo.* 当库,外挂 dataset/loss/loop |
✅ 模型侧是发布原件,可载权重闭环验证;只新写通用且简单的训练层 |
- kimodo 包前向所需模块齐全,且
TwostageDenoiser.forward本就含训练分支(twostage_denoiser.py:122-130)。 - 可载入
Kimodo-SOMA-SEED-v1.1/model.safetensors跑官方推理(load_model,§3.1),作为前向正确性的 ground truth。 - 训练只需直连
TwostageDenoiser,绕开 inference-only 的Kimodo顶层类(它全程torch.inference_mode()+eval(),§3.5)。
kimodo-train/ # 新建工程,不碰 kimodo 包源码
├── (依赖) import kimodo.* # 发布原件,当作「已验证常量」
│ TwostageDenoiser / KimodoMotionRep / Diffusion / SOMASkeleton30 / Stats
├── data/seed_dataset.py # 新写:BVH → 369维特征 + 约束采样 ← §5
├── training/loss.py # 新写:q_sample 加噪 → 两阶段 forward → 7项加权 smooth-L1(含 FK) ← §4
├── training/curriculum.py # 新写:两阶段课程 + text/constraint 采样调度(dual-CFG 训练侧) ← §4
├── training/loop.py # 新写:训练循环 + EMA(0.995@10) + Adam-atan2 + ckpt ← §6
└── configs/ # 新写:hydra 配置分层 ← §6
- 当库 import(不可动):整个 kimodo 包——精确部分,逐字节对齐权重。
- 新写(通用且简单):dataset / loss / curriculum / loop / config。扩散训练 loop 本体约百行。
- 报告已给的配方:损失权重、优化器/lr/EMA、两阶段课程、约束采样模式、CFG 权重——全部来自 technical report(§4),照搬即可。详见 §6。
- license:kimodo 包是 Apache-2.0(SPDX 头确认)。import 当训练库合规。
-
inference-only 硬假设:
Kimodo._generate/denoising_step全程torch.inference_mode();Kimodo.__init__调denoiser.eval();text encoderno_grad+逐条编码;量化路径 inference-only。应对:训练直连TwostageDenoiser、显式.train()、文本特征离线缓存。详见 §3.5。
Kimodo 是一个显式运动扩散模型(在 pose 特征空间直接去噪,非 latent / 非 token 自回归),在 700 小时光学动捕(Bones Rigplay)上训练,目标是同时做到:① 接近动捕的运动质量;② 文本 + 全套运动学约束(全身关键帧 / 稀疏关节位置+旋转 / 2D waypoint / 稠密 2D 路径 / 脚接触)的精确可控;③ 大规模多样化生成。其核心是「精心设计的运动表示 + 两阶段去噪器」,约束通过直接 imputation(覆写 pose 特征)注入,无需 ControlNet 微调 / 测试期梯度引导 / RL。
| 维度 | 常见单阶段 text2motion | Kimodo-SOMA-SEED-v1.1 |
|---|---|---|
| 骨架 | SMPL-H(22 关节) | SOMASkeleton30(30 关节,输出 somaskel77) |
| 动作表示 | 局部/速度型 ~200 维,fps=20,按 heading canonical | KimodoMotionRep 369 维,fps=30,全局表示、不按 heading canonical |
| 模型 | 单阶段 8~16 层 | 两阶段 interleaved root→body,各 16 层 1024 dim,282M,端到端联合训练 |
| 文本编码器 | CLIP ViT-B/32(512 维) | LLM2Vec(4096 维,1 文本 token + 49 个 zero「register-like」额外 token) |
| 条件 | 仅文本(或推理期组合) | 文本 + 运动学约束训练期联合 imputation |
| 采样 | DDPM 100 步 | DDIM 100 步,separated dual-CFG(w_text=2, w_constr=2) |
| 后处理 | 无 | C++ IK 脚滑清理 + 约束硬化 + root 修正 |
| 训练代码 | 通常开源 | 未开源(但 technical report 给出完整配方,推理代码给出精确格式,见 §3-4) |
约束跟随是在训练期通过 imputation 学到的(report §4.2-4.3 的两阶段课程:phase-1 纯文本预训练,phase-2 加约束)。这不是推理期能拼出来的能力,必须重训。模型本体、表示、约束注入全部直接 import kimodo 原件复用(§3),只新写薄训练层,因此既能从头训练、又能逐字节对齐发布权重做闭环验证。
Kimodo 由 NVIDIA 出品,一作 Davis Rempe(物理/约束/引导扩散方向)+ 二作 Mathis Petrovich(语义/可控运动生成方向)co-first,合流 + 规模化。
来源为 technical report(research.nvidia.com/labs/sil/projects/kimodo,代码 github.com/nv-tlabs/kimodo,报告日期 2026-3-16),非 arXiv 预印本。引用时按 NVIDIA technical report 标注。
本节是整条路线的地基:搞清如何把 kimodo 包当库实例化、载权重、跑推理、做对齐验证。全部逐行读过。
load_model(...)(model/load_model.py:122-233)→ 返回 kimodo.model.Kimodo 实例(.eval())。流程:
- 解析模型名 → 权重目录(
KIMODO_MODELS_ROOT环境变量指本地,否则 HF 下载)。 - 读
model_path/config.yaml(:195-199)。 - 注入
checkpoint_dir,OmegaConf.merge+ resolve(解析掉${oc.select:checkpoint_dir}插值,把权重路径填进ckpt_path和stats_path)。 -
instantiate_from_dict(common.py:33,自定义递归_target_工厂,不是 hydra 运行时)递归实例化整棵树。 -
eval_mode=True→model.eval()。
config.yaml 结构(SEED-v1.1):
_target_: kimodo.model.Kimodo
num_base_steps: 1000
cfg_type: separated
denoiser:
_target_: kimodo.model.twostage_denoiser.TwostageDenoiser
ckpt_path: ${oc.select:checkpoint_dir}/model.safetensors
motion_mask_mode: concat
motion_rep:
_target_: kimodo.motion_rep.KimodoMotionRep
fps: 30
stats_path: ${oc.select:checkpoint_dir}/stats/motion/
skeleton: {_target_: kimodo.skeleton.SOMASkeleton30}
latent_dim: 1024, ff_size: 2048, num_layers: 16, num_heads: 8, ...-
权重:不由
Kimodo加载,而由TwostageDenoiser.__init__收到ckpt_path后自行load_ckpt(twostage_denoiser.py:66-71)。.safetensors走safetensors.torch.load_file;strip 前缀denoiser.backbone.(:70);load_state_dict(strict)。 -
stats:mean/std.npy 是 non-persistent buffer(
stats.py:62),不在 safetensors 里,由KimodoMotionRep实例化时经stats_path加载。布局必须是 split:<ckpt>/stats/motion/{global_root,local_root,body}/{mean,std}.npy(base.py:72-83)。全局stats.mean = cat([global_root(5), body(364)])= 369。
训练保存 ckpt 后仍需单独维护
stats/motion/目录;key 命名要能对齐denoiser.backbone.strip 逻辑,否则无法回灌。
kimodo_model.py:31-61:
-
self.denoiser = denoiser.eval()然后 被ClassifierFreeGuidedModel(denoiser, cfg_type)重新赋值——所以self.denoiser最终是 CFG wrapper,内层真正可训练的 denoiser 要model.denoiser.model取。 -
self.motion_rep / skeleton / fps(从 denoiser 借)、self.diffusion = Diffusion(1000)、self.sampler = DDIMSampler(diffusion)、self.text_encoder。
load_model → model(texts, num_frames, constraint_lst, num_denoising_steps, multi_prompt=True, ...)
└ Kimodo.__call__ → _multiprompt(逐段)
├ motion_rep.create_conditions_from_constraints_batched # 约束→observed_motion+mask
├ (非首段) motion_rep.inverse 取上段尾帧 → 建过渡约束
├ Kimodo._generate # 采样循环
│ ├ text_feat = text_encoder(texts)
│ ├ cur_mot = randn(B,T,369)
│ ├ diffusion.space_timesteps + calc_diffusion_vars
│ └ for i reversed(steps): with inference_mode: denoising_step
│ ├ pred_clean = self.denoiser(cfg_weight,...,cfg_type) # CFG wrapper
│ └ x_tm1 = self.sampler(use_timesteps, motion, pred_clean, t) # DDIM
├ motion_rep.unnormalize → (post_processing) post_process_motion
└ cat 各段 → motion_rep.inverse → output_to_SOMASkeleton77 → numpy
构造一次即可训练/推理的对象(训练要复用):
-
TwostageDenoiser(内层真正可训练nn.Module,forward已含训练分支)。 -
KimodoMotionRep/MotionRepBase(编解码 + stats normalize/unnormalize)。 -
Diffusion(num_base_steps=1000,提供q_sample加噪与 schedule)。 -
SOMASkeleton30、Stats。
inference-only(训练要绕过/替换):
-
ClassifierFreeGuidedModel(采样期 guidance wrapper)→ 训练直连内层 denoiser,自己做 cond-dropout。 -
DDIMSampler→ 训练不用。 - text encoder 的服务化/量化路径(
TextEncoderAPI/LlamaEmbeddingsAPI/llm2vec_int8)→ 训练用 fp32/bf16LLM2VecEncoder或离线缓存特征。
硬编码 inference-only 假设(阻碍点 + 应对):
| 阻碍 | 位置 | 应对 |
|---|---|---|
_generate/denoising_step 全程 inference_mode()
|
kimodo_model.py:108,633 |
不走 Kimodo.__call__/_generate,直连 TwostageDenoiser.forward 自建训练循环 |
__init__ 里 denoiser.eval() + load_model(eval_mode=True)
|
kimodo_model.py:41 |
训练前显式 denoiser.train()
|
text encoder no_grad+逐条编码 |
llm2vec_wrapper.py:226 |
冻结 + 离线缓存 text_feat |
| stats 是 non-persistent buffer | stats.py:62 |
训练 ckpt 外单独维护 stats/motion/
|
- 设
KIMODO_MODELS_ROOT→load_model("Kimodo-SOMA-SEED-v1.1", device, eval_mode=True)。 -
seed_everything固定 → 跑model(texts, num_frames, num_denoising_steps=100, num_samples=1, multi_prompt=True, return_numpy=True)→ 存output["posed_joints"]作 ground truth。 - 用薄训练层自己构造
TwostageDenoiser(ckpt_path=None),灌入同一model.safetensors(stripdenoiser.backbone.),.eval()+ 关 cond-dropout,确认前向pred_clean与官方数值一致。 - 校验 stats:
<ckpt>/stats/motion/{global_root,local_root,body}/{mean,std}.npy存在,且拼接长度 ==motion_rep.motion_rep_dim(base.py:81断言)。
通过即证明「前向 + motion_rep + 归一化」100% 正确,之后训练层出错可立即定位。
from kimodo.skeleton import SOMASkeleton30
from kimodo.motion_rep import KimodoMotionRep
from kimodo.model.twostage_denoiser import TwostageDenoiser
from kimodo.model.diffusion import Diffusion
from kimodo.model.llm2vec import LLM2VecEncoder # 冻结/缓存
from kimodo.model import load_checkpoint_state_dict # 权重对齐skeleton = SOMASkeleton30()-
motion_rep = KimodoMotionRep(skeleton, fps=30, stats_path="<ckpt>/stats/motion/")# 触发 mean/std 加载 denoiser = TwostageDenoiser(motion_rep=motion_rep, motion_mask_mode="concat", ckpt_path=None, latent_dim=1024, ff_size=2048, num_layers=16, num_heads=8, llm_shape=[1,4096], num_text_tokens_override=50, input_first_heading_angle=True, ...)-
diffusion = Diffusion(num_base_steps=1000);text 用冻结LLM2VecEncoder离线缓存 - 训练循环(§4):
denoiser.train()→motion_rep编码+normalize GT →diffusion.q_sample加噪 →denoiser.forward(...)→ loss
训练 loop 本体简单,但「输入/输出/损失的精确格式」必须与发布权重一致。以下全部从推理代码反推得到,是可照实现的规格。
-
噪声调度:cosine,
alpha_bar(t)=cos((t+0.008)/1.008·π/2)²,max_beta=0.999,num_base_steps=1000(report 确认训练用 T=1000)。 -
前向加噪
q_sample:标准x_t = sqrt(ᾱ_t)·x0 + sqrt(1-ᾱ_t)·noise。训练直接调diffusion.q_sample。 -
预测目标:x0(
DDIMSampler里pred_xstart当 x0;report §4.3 也是 DDPM 简化损失的 x0 形式)。 - 损失类型:smooth-L1(loss 小用 L2 项、否则 L1,Girshick Fast R-CNN 式),不是纯 MSE。
损失按运动表示的每个分量分别算 smooth-L1,再加一项 FK 几何项,共 7 项加权:
L = λ1·||r̂_pos − r_pos|| (smoothed root position)
+ λ2·||r̂_head − r_head|| (root heading [cos,sin])
+ λ3·||ĵ_pos − j_pos|| (joint positions)
+ λ4·||ĵ_vel − j_vel|| (joint velocities)
+ λ5·||ĵ_rot − j_rot|| (joint 6D rotations)
+ λ6·||f̂ − f|| (foot contacts)
+ λ7·||FK(ĵ_rot) − j_pos|| (前向运动学一致性)
权重:λ1=λ3=λ5=10.0,λ2=2.0,λ4=3.0,λ6=4.0,λ7=5.0。
- FK 项
FK(ĵ_rot)用skeleton.fk把预测的 6D 旋转转回关节位置,与 GT 位置对齐——直接 importSOMASkeleton30.fk+motion_rep切片即可实现。 - 变长 batch:按各样本 length mask 后再求均值。
- 两阶段的损失分摊到对应分量:root 段贡献 λ1+λ2,body 段贡献 λ3~λ7(见 §4.2 的 369 维切分)。
这一节是确定事实(report 明确列出权重数值),不再是「先用等权起跑再调」。实现时按上述权重直接配置。
forward 第 122-152 行直接揭示训练行为,无需猜:
convert_ctx = torch.no_grad() if self.training else contextlib.nullcontext()
with convert_ctx:
root_motion_local = self.motion_rep.global_root_to_local_root(root_motion_pred, ...)
if self.training:
root_motion_local = root_motion_local.detach() # 训练时切断梯度
body_x = x[..., self.motion_rep.body_slice]
x_new = torch.cat([root_motion_local, body_x], axis=-1) # body 输入 = 预测 local root + 当前 body
...
output = torch.cat([root_motion_pred, predicted_body], axis=-1)确定的事实:
-
单次联合前向(同一
forward含self.training分支,非分开训两个模型)。 - Stage 2 root 输入用 Stage 1 的预测 root(free-running,非 teacher forcing)。
- 训练时 root→local 转换
.detach():梯度不从 body 回流到 root。root 分支只由自身 loss 训,body 把 root 预测当常数。 - 输出 =
cat([root_pred(5), body_pred(364)])= 369,损失按 §4.1b 的 7 个分量加权(root 段贡献位置+heading 两项,body 段贡献关节位置/速度/旋转/接触/FK 五项)。 -
维度:concat 模式下 root stage 输入 =
motion_rep_dim*2(x 与 mask 拼接),输出 =global_root_dim(5);body stage 输入 =local_motion_rep_dim + input_dim,输出 =input_dim - global_root_dim(364)。
两段是否分阶段 warmup 由 §4.3 的两阶段课程决定(phase-1 纯文本、phase-2 加约束),不是 loss 权重层面的调度——损失权重全程固定为 §4.1b 的数值。
优化器/调度(report 直接给出):
- 优化器 Adam-atan2,学习率 2e-5。
- diffusion 训练步数 T=1000;总训练 1,000,000 步。
- batch 2048(最佳模型,16×A100 SXM4-80GB;§6 的消融模型多在 8 GPU / batch 1024 / 20 fps,最佳模型为 16 GPU / 30 fps)。
- EMA:每 10 步更新,decay 0.995,测试期用 EMA 权重。
- GT 裁到最长 10 秒;首帧 root 平移到原点上方;首帧 heading 随机化(配合
c_dir,§4.4)。 - 变长序列同 batch,损失按 length mask(§4.1b)。
两阶段课程:
- Phase 1(前 500k 步):纯 text-to-motion,不喂任何约束;用 dropout 0.1。
-
Phase 2(后 500k 步):text + 运动学约束混合训练;移除 dropout(以免把直接覆写进输入的约束 drop 掉)。
- 约束从预定义模式随机采样:全身关节位置稀疏关键帧 / 手脚位置+旋转的随机子集稀疏关键帧 / 2D root 位置+heading 稀疏关键帧 / 2D root 位置+heading 稠密路径 / 脚接触配置稀疏关键帧。
- 2 个约束模式混合的概率 25%;完全不给约束(只留文本)的概率 10%。
- 稀疏约束的关键帧最大数从 1 线性增长到 20,且采样偏向更少关键帧(贴近真实使用)。
- 全程:文本以 10% 概率 drop(CFG)。
消融「No Train Curriculum」(直接 1M 步混训、text-only 与 text+constraint 等概率、无 dropout)文本/质量相当但约束误差上升——两阶段课程是被报告验证过的设计,照搬即可。
twostage_denoiser.py:98-103(motion_mask_mode="concat"):
if motion_mask is None or observed_motion is None:
motion_mask = torch.zeros_like(x); observed_motion = torch.zeros_like(x)
x = x * (1 - motion_mask) + observed_motion * motion_mask # 约束位置钉成观测值
x_extended = torch.cat([x, motion_mask], axis=-1) # mask 作额外输入通道训练时 observed_motion/motion_mask 由 motion_rep.create_conditions(§5.3)生成并作为输入通道喂入——这是「约束侧」在训练期就联合学习的实现。
每个 stage 是 TransformerEncoderBlock,注入为 prefix-token 拼接(非 AdaLN/cross-attn)。固定 num_text_tokens=50 的内部构成(report §4.2 揭示):
- text:1 个 LLM2Vec 文本嵌入(R⁴⁰⁹⁶)经
Linear(llm_dim→latent)→ 1 token。 - extra/register:49 个全零 token(report 称
c_extra ∈ R^{49×4096},非学习,但起到类似 register token 增容的作用)→ 与文本 token 一起填满 50 的固定长度。消融「No Extra Tokens」显示去掉它会损害 R-precision 与 FID。 - timestep:
TimestepEmbedder(PE 取值 → Linear-SiLU-Linear)→ 1 token。 - first_heading_angle(
c_dir):Linear(2→latent)→ 1 token(input_first_heading_angle=True)。配合训练期随机首帧 heading 增广(§4.3),让模型能从任意朝向起手、测试期由c_dir指定。 - 拼接
xseq=cat([text(1)+extra(49), time, (heading), motion])→ 标准TransformerEncoder→ 取 motion 段 →output_linear。 - 位置编码:非学习正弦 PE(
max_len=5000),加在整条 xseq 上(含 prefix)。 - 显式禁用 nested tensor / flash attention(
:224,有 bug)。
注意:「50 token」不是 50 条文本 token,而是 1 文本 + 49 零 register。训练离线缓存文本特征时缓存的是单个 4096 维嵌入,零 token 在模型内部补齐,无需缓存。
采样期 separated 三路揭示训练期必须能独立产出「有文本无约束 / 有约束无文本」两路:
text_feat = cat([text_feat, 0*text_feat, 0*text_feat]) # 路2/3 文本置零
motion_mask = cat([0*motion_mask, motion_mask, 0*motion_mask]) # 路1/3 约束置零(mask清零→不钉值)
# out_text(有文本无约束) / out_constraint(有约束无文本) / out_uncond(都无)
out = out_uncond + w_text*(out_text - out_uncond) + w_constraint*(out_constraint - out_uncond)- 文本置空 =
0*text_feat+ text pad_mask 全 False;约束置空 =0*motion_mask(清零→不钉值)。 -
训练侧的具体调度(report §4.3,确定值,非待定):
- 文本 dropout 全程 10%(两阶段都做),用于 classifier-free guidance。
- 约束的「有无」由两阶段课程控制,不是对称的 per-feature dropout:phase-1(前 500k 步)纯文本、不喂约束;phase-2(后 500k 步)才混入约束,且其中 10% 的步数不给约束(只留文本)。
- phase-1 用 dropout=0.1,phase-2 移除该 dropout(避免把直接覆写进输入的约束 drop 掉)。
- 因此
training/curriculum.py要实现的是「两阶段切换 + 约束采样调度(含 10% 无约束)+ 全程 10% 文本 drop」,而非两个独立的 drop 概率。详细约束采样模式见 §4.3 课程与 §5.3。
denoiser.train()
feats = motion_rep(local_rot, root_pos, to_normalize=True, lengths=L) # GT → 369维 (§5)
# 按当前 phase 采样约束:phase-1 无约束;phase-2 按预定义模式采样,10% 步无约束
obs, mask = sample_constraints(motion_rep, feats, phase) # §4.3 课程 + §5.3
# 全程 10% drop 文本(CFG);phase-1 额外 0.1 dropout,phase-2 不 drop 约束
text_feat, text_pad = drop_text(cached_text_feat, p=0.10) # §4.5
t = randint(0, 1000, (B,))
noise = randn_like(feats)
x_t = diffusion.q_sample(feats, t, noise) # §4.1
pred = denoiser(x_t, x_pad, text_feat, text_pad, t, first_heading_angle, mask, obs) # §4.2
loss = weighted_smooth_l1_7terms(pred, feats, skeleton) # §4.1b(含 FK 项)训练标签必须按 kimodo 的同一算法生成,否则与权重不兼容。好消息:编解码逻辑全在
KimodoMotionRep,直接 import 复用,无需手抄。数据集说明:Kimodo 官方训练集是 Bones Rigplay(700 小时光学动捕,170 人,原生 27 关节,retarget 到 SOMA/G1/SMPL-X),非公开。发布的 SOMA-SEED 系列 checkpoint 即在其 SOMA retarget 版上训练;本方案复现时以 SEED checkpoint 的 SOMA30 表示为对齐基准,自有数据(任意 BVH 动捕)按本节算法编码到同一 369 维空间即可训练。下文「BVH」泛指接入的动捕源。
| block | 形状 | 通道区间 | 含义 |
|---|---|---|---|
| smooth_root_pos | [3] | [0:3](x=0,y=1,z=2) | 平滑后 root 位置 |
| global_root_heading | [2] | [3:5] | root 朝向 (cos,sin) |
| local_joints_positions | [30,3] | [5:95] | 相对平滑 root 的关节位置 |
| global_rot_data | [30,6] | [95:275] | 全局关节旋转(6D,矩阵前两列) |
| velocities | [30,3] | [275:365] | 关节速度 |
| foot_contacts | [4] | [365:369] | 脚接触 [左跟,左趾,右跟,右趾](左侧在前) |
- 369 是算出来的(
3+2+90+180+90+4);若用 SOMA77(J=77)则 D=933,布局公式不变。canonical 骨架须从 checkpoint/stats 维度反查,不从源码假设。 - split:root 段 = [0:5](global_root_heading 是
last_root_feature),body 段 = [5:369];另有local_root_dim=4(rot_vel 1 + vel_xz 2 + y 1)供global_root_to_local_root用(§4.2 的两阶段转换)。
编码正向 KimodoMotionRep.__call__:FK → compute_heading_angle → get_smooth_root_pos → compute_vel_xyz → foot_detect_from_pos_and_vel → matrix_to_cont6d → normalize。
-
foot_contacts(
feet.py):(speed < vel_thres) AND (height < height_thresh),speed=3D 速度 L2,height=y 分量。阈值在调用处硬编码kimodo_motionrep.py:91:vel_thres=0.15、height_thresh=0.10。每侧只取前 2 个脚关节([:2],丢 ToeEnd)。decode 时>0.5二值化。 -
smooth_root_pos(
smooth_root.py):不是高斯/盒式滤波,是 ADMM 最小加速度优化(多重网格)。只平滑 x/z,y 原样透传,输出轴序[0,2,1]。硬编码:margin 半径0.06、smooth_signal(pos_weight=0, alpha_overrelax=1.8, admm_iters=500)(注意≠类默认 100/1.0)、stencil[-1,2,-1]、ADMM stepsize0.25·sqrt(diag_max)。不可微(内部.detach().cpu().numpy()走 SciPy LU)。 -
velocities(
feature_utils.py: compute_vel_xyz):fps·(pos[1:]-pos[:-1]),末帧复制 padding。 -
6D 旋转(
geometry.py):matrix_to_cont6d取矩阵前两列(列优先);cont6d_to_matrix用 Gram-Schmidt(固定 X → Z=X×Y_raw → Y=Z×X)。存的是 global 旋转。
smooth_root 不可微不影响训练:它只产
smooth_root_pos这块 GT 标签(离线常量),梯度只在网络输出↔GT 间流动。但它慢(CPU 逐 batch),应离线预计算并缓存 369 维特征,不要每个 epoch 重算。
生成稠密 [T,369] 的 observed_motion + bool motion_mask(标记哪些 (帧,通道) 是真约束),batched 后 [B,T,369]。约束→通道映射:
| 约束 key | 写入通道 |
|---|---|
smooth_root_2d(2D 路径/waypoint) |
通道 0(x)、2(z) |
root_y_pos |
通道 1 |
global_root_heading |
[3:5] |
global_joints_rots |
转 cont6d,按 t*J+j 展平 scatter 进 [95:275] |
global_joints_positions |
减 smooth-root 局部参考(y=0)转 local,scatter 进 [5:95];要求同帧已约束 smooth_root,否则 ValueError |
- velocities、foot_contacts 从不作约束注入(纯模型预测输出)。
- 同 (帧,通道) 多约束命中时
get_unique_index_and_data「首次写入者优先」去重(conditioning.py:18-28)。 - 约束类来自
constraints.py:Root2DConstraintSet/FullBodyConstraintSet/EndEffectorConstraintSet,各自update_constraints填 data/index dict。
训练时约束如何采样:report §4.3 已给出明确模式(见 §4.3 课程)——phase-2 才采样,模式包含全身关节位置 / 手脚位置+旋转子集 / 2D root 位置+heading(稀疏关键帧与稠密路径)/ 脚接触配置;2 模式混合 25%、无约束 10%、关键帧数 1→20 线性增长且偏向更少。把这些模式映射到本节五类约束 + §5.3 的通道写入即可,无需另行猜测稀疏度。
inverse(kimodo_motionrep.py:166-220)从 369 维还原 local_rot_mats / global_rot_mats / posed_joints / root_positions / foot_contacts / ...:
- 旋转链 global_rot↔6D↔local_rot 严格可逆;root x/z 经 smooth_root+hips_offset 重组代数可逆,y 直接保留。
- 有损环节:smooth_root 的高频成分不可反推(但 local_joints 存了相对偏移,root 重建仍闭合);foot_contacts 二值化丢连续值;velocities 在 inverse 中完全未用(冗余特征,可从位置重算)。
- 训练做几何 loss 时可用
inverse+skeleton.fk验证。
| 约定 | 值 | 影响 |
|---|---|---|
| 坐标系 | Y-up,地面 (x,z) | 源 BVH 若 Z-up 必须先转轴 |
| 单位 | 米制(阈值 0.10/0.15/0.06 均裸数值) | BVH 常用厘米,需 ÷100 |
| fps | 30 | 速度/角速度全 ×fps,stats 在该 fps 下统计;源数据须重采样到 30 fps |
| heading | (RightLeg,LeftLeg) 髋连线,atan2(dz,-dx)
|
SOMA30 hip 顺序 [right,left] |
| root | root_idx=0 (Hips),世界系全局平移 | — |
| 关节顺序 | 必须匹配 skeleton.bone_order_names
|
BVH 关节顺序不一致时用 get_skel_slice 重排,T-pose 用 to/from_standard_tpose 对齐 |
-
to_SOMASkeleton77(:246-256):以 77 关节relaxed_hands_rest_pose为底,填入 30 关节 local rot(缺失关节如手指填 relaxed rest)。 -
output_to_SOMASkeleton77(:264-283):扩展 local_rot→77,重跑 77 的 FK,foot_contacts 从 4 通道扩 6 通道(toe-end 复制 toe-base)。推理输出 somaskel77 即经此。 - 训练在 SOMA30 空间进行;导出/评测时转 77。
from kimodo.skeleton import SOMASkeleton30
from kimodo.motion_rep import KimodoMotionRep
# (BVH 解析可用 kimodo.skeleton.bvh / exports.bvh,或自备)-
skel = SOMASkeleton30()(自动从joints.p载 neutral_joints/parents/root_idx) - BVH →
(T,J,3,3)local_rot +(T,3)root_pos(关节顺序对齐bone_order_names,单位转米,重采样 30fps,Y-up) rep = KimodoMotionRep(skel, fps=30, stats_path=...)- 编码:
feats = rep(local_rot, root_pos, to_normalize=True, lengths=...)→[B,T,369](离线缓存) - 约束:
rep.create_conditions(...)→(observed_motion, motion_mask) - 解码/验证:
rep.inverse(feats, is_normalized=True)
KimodoMotionRep已内部聚合 fk / cont6d / heading / vel / smooth_root / foot_detect / Stats,直接调__call__/inverse/create_conditions即复用全部逻辑,无需手工拼装几何函数。
薄训练层里 kimodo 包没有现成代码的部分。好消息:technical report §3-§4 已给出完整训练配方,这里只需把它落成通用工程代码,无需从外部框架移植算法。
- 形态:标准 PyTorch(或 Lightning)训练 module + hydra 分层 config(沿用 kimodo 的
instantiate_from_dict风格组织模型侧,训练侧另起 config 组)。 - 必备组件(值见 §4.3):Adam-atan2(lr=2e-5)、EMA(decay 0.995,每 10 步)、变长 batch 的 length-mask collate(padding 到 batch 内最长)、ckpt +
stats/motion/同步保存(§3.2)。 - 分布式:报告最佳模型 16×A100 / batch 2048;复现可按显存缩到 §6.3 的 M/S 配置(8 GPU / 1024 或 4 GPU / 512)起步。
- 训练 step 本体 ≈ §4.6 伪代码,约百行。
- 直接实现 §4.3 的课程:phase-1(500k 步纯文本,dropout 0.1)→ phase-2(500k 步 text+约束,移除 dropout,约束按预定义模式采样)。
- 约束采样器把 §4.3 的模式映射到 §5.3 的
create_conditions五类约束:全身关节位置 / 手脚位置+旋转子集 / 2D root(稀疏 waypoint + 稠密路径)/ 脚接触。实现要点:2 模式混合 25%、无约束 10%、关键帧数 1→20 线性增长且偏向更少。 - 全程文本 10% drop(CFG)。这些都是确定值,照 §4.3/§4.5 配置即可。
- 文本增广:用 LLM(报告用 Qwen3-32B)把动作描述改写成统一句式(恒以「A [subject]...」开头)、多种详略级别。训练时在原始描述与改写之间按预定分布采样。
- 运动增广(拼接):随机配对拼接两段动作 clip 以覆盖「多动作组合」prompt;为保证拼接处过渡自然,用在未增广数据上训练的扩散模型自身生成过渡片段(自举式增广)。
- 训练采样混合:完整 clip / 单个或组合 atomic 子 clip / 拼接增广 clip × 原始文本 / LLM 改写文本,按预设分布采样。
- 复现建议:先在原始 clip + 文本改写上跑通(拼接增广依赖一个已训好的模型,属第二轮),对应消融「data size」实验也未用拼接增广(§9.2)。
- kimodo 包
model/llm2vec/已含LLM2VecEncoder+ bidirectional_llama,接入侧齐全。 - 训练:冻结 + 离线缓存 text_feat(单个 4096 维嵌入/条,见 §4.4——49 个零 register token 由模型内部补齐,不需缓存),不参与反传。
- 复现 Kimodo 不需要重训/微调文本编码器,用现成 LLM2Vec 编码即可。
- 见 §8:
MotionCorrection/C++ IK 脚滑清理 + 约束硬化 + root 修正,本地已有,编译即可。评测建议关闭以测裸模型。
复现训练后用官方协议验收。指标函数可直接 import 复用。
Motion quality(无需 GT)(metrics/foot_skate.py,fps=30,dt=1/fps):
-
foot_skate_from_height:仅趾,趾 y<0.05m 视为触地,触地帧趾速度均值(m/s)。 -
foot_skate_from_pred_contacts:用模型foot_contacts加权趾速度均值(results.md 显示的 Skate 列,×100 cm/s)。 -
foot_skate_max_vel:触地帧最大滑动速度尖峰。 -
foot_skate_ratio(借鉴 GMD):height_thresh=0.05、vel_thresh=0.2m/s,当前+下一帧都触地才算 on-floor,滑动帧占比。 -
foot_contact_consistency:vel=0.15/height=0.10启发式接触 vs 模型预测的 XOR 一致率,1.0 满分。
Constraint following(无需 GT)(metrics/constraints.py,ContraintFollow,root_threshold=0.10m,全 L2):
-
constraint_root2d_err/_acc:root XZ vssmooth_root_2d,acc=误差≤0.10 占比;_p95在evaluate_folder._to_p95用nanquantile(0.95)聚合。 -
constraint_fullbody_keyframe:关键帧全关节 L2 均值。 -
constraint_end_effector:仅pos_indices(旋转约束 "EE Rot" 列永远 None,未实现)。
Text alignment(部分需 GT)(metrics/tmr.py + model/tmr.py):
- 相似度
cosine/2+0.5∈[0,1];R-precision R01-R10 =100·(rank<k),near-dupthreshold=0.99。 -
FID = TMR 256 维 latent 上的 Fréchet(无独立 inception 网络)。
FID/gen_gt(需 GT,文献常报)/FID/gen_text(需文本不需 GT)。 - 用 TMR-SOMA-RP-v1(HF
nvidia/TMR-SOMA-RP-v1,经load_model(default_family="TMR")加载),内部用 LLM2Vec 编码文本,bf16 默认(v1.1 fp32)。 -
TMR 用的是
TMRMotionRep(位置型、旋转不变:去 heading 的 local 位置+速度+脚接触,无 6D 旋转),与生成用的KimodoMotionRep不同。
-
create_benchmark.py:HF 拉 testsuite 元数据 + 读 BONES-SEED BVH → 30fps 子采样 →to_standard_tpose→KimodoMotionRep→ crop+canonicalize+inverse → 写gt_motion.npz;有seed_constraints.json则派生constraints.json(~26GB,数小时,文档值未实测)。 -
generate_eval.py:跑模型 →motion.npz(须含posed_joints+foot_contacts,per-case seed,batch=1,后处理建议 OFF)。 -
embed_folder.py:TMR-SOMA 嵌入 motion/gt/text →.npy。 -
evaluate_folder.py:两遍——generated pass(全 6 指标含 TMR)+ GT pass(仅 motion-quality + constraints,TMR 不重算)。SOMA30 自动升 77。 -
parse_folder.py:按 split×category 加权平均,位置×100→cm,skate×100→cm/s。
-
content:held-out 语义类别(
content_name训练未见)→ 测文本泛化。 - repetition:见过类别的新片段(文本已见)→ 测约束跟随泛化。
- 类别:text-following(overview/timeline_single/timeline_multi)+ constrained(with/without text,各含 root/fullbody/end-effectors/mixture)。全套 22,474 case。
| 指标 | GT 上界 | Kimodo-SEED-v1.1 | 说明 |
|---|---|---|---|
| FID/gen_gt | 0.000 | ~0.03(content)/ ~0.005(repetition) | 越低越好 |
| R@3 (Overview) | 89-94% | 73-92% | 检索上界即 GT |
| Skate (cm/s) | ~1.7-2.1 | ~3.3-4.6 | 越低越好 |
| Contact consistency | 1.000 | ~0.97-0.98 | — |
| 约束位置误差 (cm) | ~0.000 | FB ~1.1-1.3 / EE ~1.4-1.8 / 2DRoot ~2.6-3.1 | GT 生成了约束故≈0 |
注:本表为发布的 SEED-v1.1 benchmark(
results.md,SOMA 30fps)数值。technical report 的 Tab.1/Tab.2 是在原生 27 关节、20fps 上的消融/scaling 结果(report 明确声明二者不可直接比较);report §2.2 另给 SOMA 30fps 最佳模型的约束误差(FB 3.21cm / EE-pos 3.63cm / EE-rot 6.88° / root 3.63cm),口径与本表不同,勿混用。
-
import:
kimodo.metrics的FootSkate*/FootContactConsistency/ContraintFollow/TMR_EmbeddingMetric+compute_metrics/aggregate_metrics;kimodo.constraints.load_constraints_lst;SOMASkeleton77/30。 - 外部模型:仅 TMR-SOMA-RP-v1(任何 TMR 指标都需要,内部依赖 LLM2Vec)。
-
数据:GT 由
create_benchmark.py从 BONES-SEED BVH + benchmark 元数据构建。 -
无 GT 可算:全部 foot-skate / constraint /
FID/gen_text/t2m_R。必须 GT:FID/gen_gt、m2m_*、t2m_gt_*、所有per_motion_mean_gt上界行。
postprocess.py: post_process_motion(:186)+ C++ 库 MotionCorrection/(C++17 IK + 轨迹校正,Python 绑定):
- 输入:生成的 local 旋转、root 位置、foot_contacts、约束列表。
- 功能:脚滑清理(按接触标签锁脚 + IK 反解)、约束硬化(扩散软约束 → 后处理硬性满足)、root 修正(
root_margin=0.04,偏离超阈值校正轨迹)。 - 多段拼接按段后处理(
kimodo_model.py:296)。 - 训练不涉及;本地已有,编译即可,推理/评测时按需启用(评测建议 OFF 以测裸模型)。
- 噪声调度 cosine(max_beta=0.999, base_steps=1000)、
q_sample加噪、x0 预测(§4.1)。 - 损失:7 项加权 smooth-L1(含 FK 项),权重 10/2/10/3/10/4/5(§4.1b,report §4.3 直接给出)。
- 两阶段单次联合前向、Stage 2 用预测 root(free-running)、root→body 梯度
.detach()(§4.2)。 - 训练超参:Adam-atan2 / lr=2e-5 / 1M 步 / batch 2048(16 GPU) / EMA decay 0.995@10 步 / 裁 10 秒 / 随机首帧 heading(§4.3,report §4.3)。
- 两阶段课程:phase-1 500k 纯文本(dropout 0.1) → phase-2 500k text+约束(去 dropout);2 模式混 25%、无约束 10%、关键帧 1→20;全程文本 drop 10%(§4.3/§4.5,report §4.3)。
- text 注入 = 1 文本 token + 49 零 register token + time + heading,prefix 拼接 + 正弦 PE(§4.4,report §4.2)。
- dual-CFG 采样 w_text=2 / w_constr=2,DDIM 100 步(§4.5,report §4.4)。
- 表示布局、训练标签算法(foot 0.15/0.10、smooth_root ADMM/margin 0.06)、约束→通道映射、编解码、坐标系/单位/fps(§5)。
- 模型装载/权重对齐/闭环验证(§3)。
- 数据集本身:官方 Bones Rigplay(700h,非公开)无法直接获得;复现需用自有动捕,规模与多样性直接决定约束跟随上限(report §6.3 显示数据量主导约束精度)。
- 数据采样分布细节:完整 clip / 子 clip / 拼接增广 / 原始文本 vs LLM 改写之间的具体采样比例,report 只说「按预定分布」,未给数值——这是少数真未知。
- scaling 取舍:受算力约束选 L/M/S 模型与 4/8/16 GPU 配置(report §6.3 给了趋势与对应指标,可据此取舍)。
与早期判断不同:lr/batch/步数/EMA/损失权重/课程/dropout 均非未知,report §4.3 已明确给出,直接照搬。真未知只剩「数据本身 + 采样分布的具体比例 + scaling 取舍」。
- 训练 fps 与世界单位(§5.5;config 标 30/米制,须与 stats 口径一致)。
- canonical 骨架 J=30/77(§5.1;从 checkpoint 维度反查,369 vs 933)。
- smooth-L1 的 beta(L1↔L2 切换阈值)是否取 PyTorch 默认 1.0(report 未给,按默认起跑)。
状态 ⬜ 未精读 / 🟡 已概览待精读 / ✅ 已精读。包根:
...\NvlabKimodoQuickServer1\kimodo\kimodo\
| 状态 | 文件 | 结论位置 |
|---|---|---|
| ✅ |
model/load_model.py + loading.py + registry.py + common.py
|
装载/权重/stats(§3.1-3.2) |
| ✅ | model/kimodo_model.py |
顶层构造 + 推理链 + inference-only 假设(§3.3-3.5) |
| ✅ | model/twostage_denoiser.py |
维度 + forward + 训练耦合(§4.2) |
| ✅ | model/diffusion.py |
cosine + q_sample + x0 + DDIM(§4.1) |
| ✅ | model/cfg.py |
dual-CFG 三路 → 训练期文本 drop + 约束课程采样(§4.5) |
| ✅ | model/backbone.py |
prefix-token 注入 + 正弦 PE(§4.4) |
| ✅ | scripts/generate.py |
端到端调用链(§3.4) |
| 状态 | 文件 | 结论位置 |
|---|---|---|
| ✅ | motion_rep/reps/kimodo_motionrep.py |
布局 + 编解码 + create_conditions(§5.1-5.4) |
| ✅ | motion_rep/reps/base.py |
slice_dict + normalize split(§5.1) |
| ✅ | motion_rep/stats.py |
(x-mean)/sqrt(std²+eps), eps=1e-5(§3.2) |
| ✅ | motion_rep/smooth_root.py |
ADMM 平滑,margin 0.06,不可微(§5.2) |
| ✅ | motion_rep/feet.py |
vel 0.15/height 0.10,每侧前 2 关节(§5.2) |
| ✅ | motion_rep/conditioning.py |
observed/mask 布局 + 首次写入优先(§5.3) |
| ✅ | motion_rep/feature_utils.py |
heading/vel/RotateFeatures(§5.2,5.5) |
| ✅ | geometry.py |
6D=前两列,Y-up(§5.2,5.5) |
| ✅ | skeleton/{kinematics,transforms,definitions,base}.py |
FK + SOMA30↔77(§5.5-5.6) |
| 🟡 | constraints.py |
5 类约束已枚举(§5.3);待补 EndEffector 的 bone_index/pos_indices |
| 状态 | 文件 | 结论位置 |
|---|---|---|
| ✅ | metrics/{foot_skate,constraints,tmr,base}.py |
三轴指标精确算法(§7.1) |
| ✅ |
model/tmr.py + motion_rep/reps/tmr_motionrep.py
|
TMR 加载 + 表示差异(§7.1) |
| ✅ | docs/source/benchmark/{pipeline,results,metrics,introduction}.md |
测试集构建 + 对标数值(§7.2-7.4) |
| 状态 | 文件 | 结论位置 |
|---|---|---|
| 🟡 |
postprocess.py + MotionCorrection/ C++ |
接口概览(§8);待补 C++ 参数细节 |
| ⬜ | model/llm2vec/* |
接入侧齐全;文本特征 = 单个 4096 维嵌入/条(49 零 register 由模型补齐,§4.4),训练用缓存,优先级低 |
-
第 0 步 闭环验证(§3.6):
load_model跑通官方推理 → 自构TwostageDenoiser灌权重 → 确认前向数值一致。这是地基,不过不往下走。 -
数据管线(§5):BVH → 对齐坐标系/单位/fps →
KimodoMotionRep编码 → 离线缓存 369 维特征 + LLM2Vec text_feat 缓存。验证:inverse往返一致。 -
薄训练层(§4):实现
loss.py(7 项加权 smooth-L1 含 FK,§4.1b)+curriculum.py(两阶段课程 + 约束采样 + 10% 文本 drop,§4.3/§4.5)+loop.py(Adam-atan2 + EMA,§4.3/§6.1)。先按 phase-1 text-only 跑通,再开 phase-2 约束。 - 过拟合自检:小批数据上能否过拟合 + 用官方 stats 解码出合理动作。
- 评测对齐(§7):建 content/repetition 测试集 → 复刻指标 → 对 §7.4 基线。
- 规模化训练(§9.2):按 §4.3 配方(1M 步、batch 2048/16 GPU、两阶段课程)放量;唯一靠实验微调的是数据采样分布与 scaling 取舍。
一句话:模型、表示、训练配方均已可照实现(§3-5 为【源码】,§4.3/§4.1b 训练超参为【报告】明确给出),训练层是通用薄壳。先做闭环验证拿到 ground truth,再逐层往上搭。