smolvla

主要是代码理解和修改

state在整个结构里面处在什么位置 SmolVLA 由一个紧凑的预训练视觉语言模型组成,丢弃最后的 L − N 层(剪刀图标)。其余层嵌入了三个输入:(i)语言指令,(ii)RGB图像,以及(iii)机器人感觉运动状态。它们合并的标记为交替交叉注意力(金色)和自我注意力(浅黄色)块的动作专家提供信息,这些块通过流匹配进行训练,以输出 n 个低级动作块,在 . . . , at+n。SmolVLA 在公共社区数据集上进行预训练,并在低成本机器人上进行评估。

train

  1. 除了基础参数外,我需要加的
    1. 训练的时候用/不用深度图,需要在输入特征里面
    2. 训练的时候用哪一种language方法
      1. "":baseline
      2. mtask_relative
      3. mtask_grid_xxcm
      4. add_task_to_state好像得单独一个

主函数

def train(cfg: TrainPipelineConfig):

validate

对policy_path

从 CLI(命令行参数)里获取 --policy 参数对应的路径。会再解析命令行里的 policy 相关覆盖参数(cli_overrides)。用 PreTrainedConfig.from_pretrained(...) 加载 policy 的配置(只加载配置,不加载权重)。记录 self.policy.pretrained_path = policy_path。

也就是当train的cfg有policy的时候,cfg.policy=PreTrainedConfig.from_pretrained(policy_path...)创建了一个类,包含的通常是超参数、路径、模型结构信息等,然后 self.policy.pretrained_path = policy_path。

此时还没有加载模型的配置,那模型的输入特征是在这里决定的吗?

--policy.path=models/forsmolvla/smolvla_base这个是训练的时候的设置,所以只是加载预训练的smolvla_base的输入设置

我看了这个smolvla_base的input feature规定state是6维,所以我如果想fine-tune的话,得改这个base的设置🤔

dataset

dataset = make_dataset(cfg)

应该就是提取cfg里面dataset的部分吧?感觉训练的时候关于datset的处理(用不用深度/文本处理应该写在dataset里面,而不是写在train.py里面)

这个要改就得改lerobotdataset的处理,希望可以自定义一个配置类,先这么些

  1. lerobotdataset类的init新增
    1
    2
    3
    4
    5
    # 下面是为了train的自定义内容
    language_tip_mode: str="", # 当空的时候就等于baseline
    add_location_to_state: bool = False, # 控制是将location加到state里面
    exclude_features: list[str] = None, # 控制需要过滤的 key,最终的训练(主要是为了过滤掉depth)
    obj_detector=None, # 可选的目标检测器,为了state加的
  2. 把 FilteredBatchLoader.add_location_to_state 移到 lerobotDataset里面:_add_location_to_state函数(dataset是无辜的要怪就怪模型结构),调用加在geitem函数里面
  3. filter加载getitem函数的最后,return 之前
  4. make_dataset传入参数
  5. train清理之前的config,传入参数到make_dataset里边
  6. 正向流程
    1. 整理train cfg的模板,增加,同样config/train.py里面也要修改
      1
      2
      language_tip_mode: str="", # 当空的时候就等于baseline
      add_location_to_state: bool = False, # 控制是将location加到state里面
    2. train里面dataste=make_dataset(cfg)
    3. 调用lerobotdataset初始化
    4. getitem得到一帧对应的color,depth,state,force,action等
    5. 返回一帧的color,state,action
  7. 测试:
    1. 能不能正常train baseline
    2. 能不能正常train language_mode 看task就行
    3. 能不能train state(显然不行) 看state

policy

  1. 用cfg.policy(就是validate的时候cfg.policy=PreTrainedConfig.from_pretrained(policy_path...)创建的类)和dataset.meta创建policy
    1
    2
    3
    4
    policy = make_policy(
    cfg=cfg.policy,
    ds_meta=dataset.meta,
    )
  2. make_policy用meta干什么,meta也要修改,但不能在lerobotdataset里面,而是make_policy的时候。这个policy是我最后要得到的policy吧。meta就规定了input feature和output feature
    1. 过滤depth和force这两个feature
    2. 当且仅当state要求改的时候才改
  3. policy = policy_cls.from_pretrained(**kwargs)
    1. smolvla里面没有代码,应该调用的是Pretrainedpolicy这个基类的from_pretrained
    2. 能保证传入的kwargs是正确的。然后instance = cls(config, **kwargs)是生成SmolVLAPolicy实例的,为什么这里的cls里面已经没有10那个信息了?
      1. cls 实际上是 SmolVLAPolicy 类的初始化,初始化里面有一个加载空flowmatching模型的,就是cls.model=VLAFlowMatching(config)。然后VLAFlowMatching的初始化里面有self.state_proj = nn.Linear( self.config.max_state_dim, self.vlm_with_expert.config.text_config.hidden_size )。
      2. 所以instance的结果里面只有两个包含state的内容
        1. SmolVLAPolicy( (normalize_inputs): Normalize( (buffer_observation_state): ParameterDict( (mean): Parameter containing: [torch.FloatTensor of size 6] (std): Parameter containing: [torch.FloatTensor of size 6] ) )这个是预训练模型的前6维的权重,后面还需要用
        2. (state_proj): Linear(in_features=32, out_features=960, bias=True)这个是投影到32维
  4. from_pretrained里面policy = cls._load_as_safetensor(instance, model_file, config.device, strict)
    1. cls还是 SmolVLAPolicy,就是调用_load_as_safetensor这个class method
    2. 用的是smolvlapolicy里面的_load_as_safetensor
      1. safetensors.torch.load_model(model, model_file, strict=strict, device=map_location)把权重加载到模型实例里
      2. return load_smolvla( model, model_file, device=map_location, checkpoint_keys_mapping="model._orig_mod.//model.", )调用特定的 loader 做额外处理
  5. load_smolvla函数,我需要在这里确保state的权重能1.前6位正常加,7-10位初始化一下
    1. 有没有办法识别input feature此时是6还是10?目前这个模型传到这里只剩下cls两个关于state的部分了,所以从外部引入
    2. 给7-10维重置,变成用 Xavier 均匀初始化覆盖,bias 清零

train

  1. policy.train()运行的是SmolVLMWithExpertModel的train,设置冻结哪一块
  2. 开始按步数训练 train_tracker, output_dict = update_policy(policy)
  3. update_policy函数
    1. 训练逻辑
      1
      2
      3
      4
      5
      6
      loss, output_dict = policy.forward(batch)
      grad_scaler.scale(loss).backward()
      grad_scaler.unscale_(optimizer)
      torch.nn.utils.clip_grad_norm_(policy.parameters(), ...)
      grad_scaler.step(optimizer)
      optimizer.zero_grad()
      问题可能发生在,state更新参数的时候7-10维变化比较大
  4. normalize归一化没有匹配维度
    1. modeling_smolvla里面SmolVLAPolicy的init有self.normalize_inputs = Normalize(config.input_features, config.normalization_mapping, dataset_stats)
    2. state前6维的mean,std是从dataset_stats里来的,dataset_stats是从SmolVLAPolicy的init传入的
    3. 之前make_policy传入的kwargs["dataset_stats"] = meta_for_policy.stats,相当于ds_meta.stats
    4. ds_meta是ds_meta=dataset.meta,所以还是本地传入的,看到episode_stats.jsonl文件里面有类似min,max,std的,get
      1. 怎么写入save_episode有一个write_episode_stats函数
      2. 怎么读 直接从meta里面读的,这个我好像改不了
      3. 怎么计算:ep_stats = compute_episode_stats(episode_buffer, self.features)
      4. 形式: "observation.state": {"min": [-47.14912414550781, -96.49805450439453, 9.134847640991211, -1.6986300945281982, 2.4175825119018555, 0.6323396563529968], "max": [8.552631378173828, 44.06614685058594, 99.51667785644531, 42.136985778808594, 52.91819381713867, 56.36856460571289], "mean": [-13.58438491821289, -31.481496810913086, 56.781070709228516, 22.7191219329834, 25.275249481201172, 15.44166374206543], "std": [19.134763717651367, 52.86572265625, 35.76657485961914, 15.830253601074219, 18.950584411621094, 18.50875473022461], "count": [181]},也就是一个episode里面所有state每个维度的统计。
      5. 然后变成全局的:aggregate_feature_stats
      6. 修改过程,使得可以在算出来
        1. 在正式normalize之前,需要确保能传入datset["observation.state"]以计算
        2. batch = self.normalize_inputs(batch)这里用的是Normalize的forward,正好传入了batch
          1. 一个batch里面有什么?若干个epsiode?
          2. 怎么normalize的forward里面的mean和std又变成全局的了,全局还不好算?
          3. normalize类用create_stats_buffers这个函数把所有episode的mean和std算到一个全局的里面

VLAFlowMatching

init

  1. self.vlm_with_expert = SmolVLMWithExpertModel(...)构建带专家头的多模态主干,这个对象里包含:

    视觉编码器(如 SigLIP)+ 语言嵌入层;

    一个 Transformer(“VLM”主体);

    动作“专家(Expert)”分支以及处理器(processor / tokenizer)。

    此时的self.config.vlm_model_name="models/forsmolvla/HuggingFaceTB/SmolVLM2-500M-Video-Instruct"

    所以vlm_with_expert加载的就是冻结的vlm?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    self.vlm_with_expert = SmolVLMWithExpertModel(
    model_id=self.config.vlm_model_name,
    freeze_vision_encoder=self.config.freeze_vision_encoder,
    train_expert_only=self.config.train_expert_only,
    load_vlm_weights=self.config.load_vlm_weights,
    attention_mode=self.config.attention_mode,
    num_expert_layers=self.config.num_expert_layers,
    num_vlm_layers=self.config.num_vlm_layers,
    self_attn_every_n_layers=self.config.self_attn_every_n_layers,
    expert_width_multiplier=self.config.expert_width_multiplier,
    )

    这个代码初始化了一个什么?

  2. self.state_proj这个是把原来的32维的state投影到文本隐藏维(text_config.hidden_size),这个维度是从 self.vlm_with_expert里面来的

  3. action_in_proj / action_out_proj:将 动作维 映射到专家隐藏维 D_exp,便于送入专家分支;将专家输出再投回 动作维,得到对目标速度场的预测

  4. set_requires_grad,如果config.train_state_proj是true的话,就会设置state_proj里面的所有参数require_grad为true,所以这里应该是true

  5. 一些关于图像的token,不关注了

  6. 一个prefix_length,默认是 prefix_length: int = -1

embed_prefix

  1. 输入state
  2. 目的:把图像、(可选的)图像特殊 token、语言 token、状态拼成一段“前缀序列”,并生成对应的 pad mask 与 跨段注意力屏蔽标记。
  3. 图像嵌入
  4. 语言嵌入
  5. 状态嵌入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 状态嵌入,要改肯定改这里,如果要改的话还要加一个,self.config里面有吗?
    # 可以用self.add_location_to_state(或者改成train_add_location_to_state)
    # 投影到self.vlm_with_expert.config.text_config.hidden_size对应的维数,把状态维度投影到 和语言/图像 token 相同的 hidden_size
    state_emb = self.state_proj(state)
    # 如果只有2维?就加个维。这里也应该肯定是2d的吧
    print("prefix_embed这里是1d的",state_emb.ndim == 2)
    state_emb = state_emb[:, None, :] if state_emb.ndim == 2 else state_emb
    # 追加到总的embs,embs从(image_end_token,lang_emb)变成(image_end_token,lang_emb,state_emb)
    embs.append(state_emb)
    # bsize就是batchsize
    bsize = state_emb.shape[0]
    device = state_emb.device
    # states_seq_len就是状态token的个数
    states_seq_len = state_emb.shape[1]
    # 这段token没有padding,形状是((B, states_seq_len))
    state_mask = torch.ones(bsize, states_seq_len, dtype=torch.bool, device=device)
    pad_masks.append(state_mask)

    # Set attention masks so that image and language inputs do not attend to state or actions
    att_masks += [1] * (states_seq_len)
    pad_mask和att_mask

pad_masks(padding mask)

作用:标记哪些 token 是“真实有效的”,哪些是 padding(补齐长度)。

取值:布尔值 True/1 = 有效,False/0 = padding。

形状:(B, L_total),对应整个序列每个位置。

att_masks:attention 域掩码

作用:告诉 make_att_2d_masks 不同 token 属于哪个“域(domain)”,从而控制谁可以 attend 谁。

这个函数最后,pad_masks是表示哪个token是有效的(state这里应该都是有效),att_masks的话,图和task是0,state是1

embed_suffix

把 动作序列 与 时间 t 融合,作为“后缀序列”,只包含动作端的 token。跟state倒是没什么关系 att_masks 为长度 T_act 的 1 序列。 配合前缀里的 0/1,通常表示“后缀的动作 token 属于另一个注意力域”,从而避免图像/文本去看动作;也可允许状态/动作之间的相互可见性(具体由 make_att_2d_masks 决定)。

为什么action也要是1?大概懂了点,要分为条件输入和决策输入,emm主要是我不知道还能放在哪儿,理论上来说这个位置信息肯定是放在0比较好的

forward

目标:学习一个 速度场 v_θ(x_t, t),去逼近真速度 u_t = ε - x_0 的等价形式(此实现里是 ε - actions),属于 flow matching/噪声驱动的动作建模思想。

SmolVLMWithExpertModel

init

  1. self.vlm和self.procesor的区别
    1. self.vlm是AutoModelForImageTextToText.from_pretrained( model_id),g会加载一个 视觉-语言-文本(VLM)大模型
    2. self.processor = AutoProcessor.from_pretrained(model_id)看起来都是from_pretrained(model_id)
    3. self.vlm是模型权重 + 前向推理逻辑,类似 transformers 里常见的 AutoModelForCausalLM。这里是 Image+Text → Text 的多模态大模型(视觉-语言模型,里面应该包含
      1. model
        1. 是整个 VLM 的 主体 backbone,直接调用 self.vlm.model(...) 会自动执行 vision_model + connector,给你一个对齐到 text hidden_size 的 embedding
        2. .vision_model视觉编码器(把图片转成 embedding)纯粹的encoder,只负责「把图像转成 patch embedding」
          1. 和VLAFlowMatching里面的prefix_embed的区别是:embed_prefix 不自己做图像编码,它只是一个「拼接器」
          2. img_emb = self.vlm_with_expert.embed_image(img)这里调用的就是self.vlm_with_experts.vlm.model
        3. .text_model:文本编码器/解码器
          1. 同理,embed_prefix里面对state和language的处理也类似,关注state:
        4. .connector:模态对齐层(把 vision hidden state 映射到 text hidden space)
      2. forward
      3. generate
    4. self.processor是输入预处理器,负责把原始数据(图像、文字字符串)转成 self.vlm 能接受的张量
      1. 对图像:resize、归一化、转 tensor
      2. 对文本:tokenize → token_id → attention_mask
  2. self.vlm.model.text_model.layers减少到前num_vlm_layers(16)层
  3. 构建更窄的 expert 部分lm_expert
    1. lm_expert_config = copy.deepcopy(config.text_config)为什么只用text_config?因为专家模型只管 text 部分(不管 vision),vision也被拼接到text里面了。先得到一个大概的config结构,和self.vlm.config.text_config是一样的大小。
    2. 更窄
      1. 原来mlp里面的结构是这样的:hidden_size → intermediate_size → hidden_size,也就是先把先把 hidden 向量投影到一个更宽的空间(intermediate_size),再投影回 hidden_size,这样模型有更强的表达能力。
      2. hudden_size (每个 token embedding 的维度,Transformer 层的输入/输出维度)
      3. intermediate_size FFN 里间层宽度,挺复杂的不用管这个经验公式
      4. num_hidden_layers 堆多少层 Transformer,因为 Expert 要“对应”VLM 的层结构,方便做跨注意力对齐。这样一层 Expert 对应一层 VLM,更容易设计 cross-attn。如果额外设置了一个num_expert_layers,vlm跟expert不是一层层对应,但是expert_layer要是16的因数,为了方便稀疏交互
    3. self.lm_expert = AutoModel.from_config(lm_expert_config)结构和 VLM.text_model 一样,但更小、更窄
    4. 大小确定了,值是需要自己训练的,也就是expert需要单独训练。
  4. atten_mode
    1. 因为 cross attention 模式下,Expert 不直接用自己的 K/V,而是用大 VLM 的 K/V 表示,改值,所以需要维度也匹配一下,不然vlm的值拿不过来:当该层做 cross attention 时 → Expert 的输入是 VLM 传下来的特征(比如 hidden_size=1024),必须经过这个替换过的 Linear,投影到 Expert 自己的宽度(比如 hidden_size=512)。。vlm不是冻结的吗,也有kv?
    2. 不是每一层 Expert 都用 cross attention,每 xx 层保留一次 纯自注意力,其他层就把 K/V 换成来自 VLM 的(投影过的)K/V,也就是cross attention和self-attention
  5. 专家不自己负责词嵌入,统一用 VLM 的词嵌入或上游传入的 inputs_embeds。self.lm_expert.embed_tokens = None
  6. set_requires_grad
    1. 如果self.freeze_vision_encoder设置为true:整个vlm冻结,expert可以训练,只更新expert的权重
    2. train_expert_only=True
  7. self.vlm,self.vlm.model,self.vlm.model.vision_encoder

forward

  1. batch_size
  2. 对每一层要么self-atten要么cross-atten,记录结果att_outputs, past_key_values
1
2
3
4
5
6
7
8
9
10
11
12
att_outputs,past_key_values=self_or_cross-attention(
model_layers, # 当前层的子模型层 (vlm 层 + expert 层) 是可训练的部分,包含全部需要更新的权重。
inputs_embeds, # 该层的输入 hidden states [vlm_embeds, expert_embeds]
layer_idx, # 当前层编号
position_ids, # 每个 token 的位置 id (padding 通常是 0 或特殊处理)
attention_mask, # 注意力 mask,控制哪些 token 能被看到
batch_size, # 批大小 (B)
head_dim, # 每个 head 的维度 (D)
use_cache, # 是否使用 KV cache (推理时加速)
fill_kv_cache, # 是否往 cache 里填新的 K/V (True=训练; False=推理复用旧KV)
past_key_values # 存储历史 KV 的字典
)

att_outputs 和 past_key_values 在整个模型中的位置

这两个是 中间结果,在模型 forward 的流水线上扮演不同角色:

att_outputs

这是这一层(vlm + expert)的 attention 输出 hidden states。

会作为 下一层的输入,一路传到最后一层 → 接 decoder head / classifier head。

所以它处于 「主干计算图」 中,梯度会往回传。

相当于 transformer 层的 hidden_states。

past_key_values

保存每一层的 KV (key, value) 张量。

在训练时 可以不用(因为我们每次都全量计算),

在推理时(自回归生成),用它来避免重复计算旧 token 的 KV。

它处于 「缓存/优化分支」,通常不会参与梯度计算。

只存储 forward 的中间结果,不会影响训练更新

训练流程

  1. train
  2. smolvlapolicy
    1. init dataset_stats和config.input_features里面的state维度要对应,改一下dataset_stats
      1. policy = make_policy(ds_meta)这个ds_meta就是dataset_stats吗?加上
    2. forward此时传入的batch的state是10维
  3. vlaflowmatching
    1. self.vlm_with_expert = SmolVLMWithExpertModel
    2. self.state_proj = nn.Linear( self.config.max_state_dim, self.vlm_with_expert.config.text_config.hidden_size )
    3. (, suffix_out), = self.vlm_with_expert.forward( attention_mask=att_2d_masks, position_ids=position_ids, past_key_values=None, inputs_embeds=[prefix_embs, suffix_embs], use_cache=False, fill_kv_cache=False, )
  4. smolvlmwithexpert
    1. init
    2. forward,传入的
      1. inputs_embeds:外部准备好一个 [batch, seq_len, hidden_dim] 的张量,它都会进入 forward 流程
      2. position_ids:这只影响 RoPE(旋转位置编码),它决定每个 token 在序列里的位置。和你要训练的 state 的值域/维度没有直接关系,只是告诉注意力计算“第 i 个 token 的位置是多少”。

dataloader

之前写在train.py里面的,for key in batch:之后,为了保存数据到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
states = batch["observation.state"].detach().cpu()
if states.dim() == 3: # e.g. [B, 1, 10] → squeeze掉
states = states.squeeze(1)
episode_indices = batch["episode_index"].detach().cpu().tolist()
frame_indices = batch["frame_index"].detach().cpu().tolist()
states_list = states.numpy().tolist()

with open("modified_states.jsonl", "a") as f: # 追加写入
for ep, fr, st in zip(episode_indices, frame_indices, states_list):
record = {
"episode_index": int(ep),
"frame_index": int(fr),
"state": st
}
f.write(json.dumps(record, ensure_ascii=False) + "\n")

evaluate

  1. 要新增的功能
    1. 服务器端推理次数统计:只要在policy_server端增加log就行,init里面搞个计数器
    2. 能测试baseline,修改task和修改state
      1. task各种形式
      2. state要proj和normalize

config

基本上处理都可以从pretrained_name_or_path里面看出来吧,只要在配置的yaml文件里面写清楚model的path就行,然后服务器根据名字选择出 mtask,mstate和baseline

  1. baseline
  2. mtask_
    1. relative
    2. grid_2cm
    3. grid_5cm
  3. mstate_
    1. relative
    2. 1m

policy_server

  1. 根据config得到控制
  2. 推理之前_predict_action_chunk里面处理state和task
  3. 推理state的时候是否还需要修改normalize?
    1. prepare_batch函数里面看到了,来改吧
    2. 之前forward函数里面有batch = self.normalize_inputs(batch,adding_state_stat)
    3. 需要确保self.add_location_to_state有对应的值
      1. self.add_location_to_state=config.add_location_to_state
      2. cfg是SmolVLAConfig,也就是模型初始化的时候要有add_location_to_state这个设置
    4. self.add_state_dim加载正确
  4. 推理的时候去掉side_depth这个多余的feature

robot_clinet

  1. 注释掉validate_robot_cameras_for_policy(lerobot_features, policy_image_features),因为本地不用这个

8.29重新整理代码,今晚收集第二个物体+整理代码

git remote add upstream https://github.com/huggingface/lerobot.git 之前更新的时候不小心丢掉了

  1. camera:realsense camera更改比较多,主要是新增保存双通道的深度图
  2. scripts/train.py
  3. configs/train.py 增加了3个命令行参数 # 自定义 # 当空的时候就等于baseline
    language_tip_mode: str = "" # 改成模式,有 pure和grid # 控制是将location加到state里面 add_location_to_state: str = "" freeze_except_7_10: bool= False

factory.py 里面make_dataset