第一次看到"0.2B 参数、32Hz 实时推理、RTX 4090 上只要 0.9GB 显存"这三个数字摆在一起的时候,我的第一反应是核对单位写错了。原因很朴素:在视觉-语言-动作(Vision-Language-Action,VLA)这条线上混过一段时间的人都知道,实时性从来不是参数量单独决定的,显存占用也从来不是一个能被简单"除以参数量"算出来的数。TurboVLA 这个组合之所以值得拿出来聊,恰恰是因为它同时踩中了三个通常互相打架的指标——模型要小、频率要高、显存要省——而它给出的答卷看起来像是把三者的矛盾给绕过去了。
这篇文章想做的事情,不是复述一遍参数表,而是把"0.2B / 32Hz / 0.9GB"这组数字背后的工程账拆开算一遍:一个控制周期里时间到底花在哪,显存到底被谁吃掉了,想在自己的机器上复现这套链路需要注意哪些细节,以及实测中最容易翻车的那几个地方。内容覆盖实时推理的延迟拆解、显存统计口径、低显存部署的取舍、动作队列与时间戳对齐、LoRA 微调的显存预算等,适合已经在跑 VLA 或者准备把某个策略模型塞进真实控制回路的读者,也适合刚接触这块、想知道"为什么大家都在抠毫秒和兆字节"的朋友。
1. 0.2B 参数跑到 32Hz,这个组合真正的难点在哪
1.1 把 32Hz 翻译成毫秒预算
讨论实时推理,最忌讳用"帧率"这个词笼统地说事,因为帧率只描述结果,不描述约束。32Hz 的意思是每 31.25 毫秒必须完整走完一次"取图—编码—推理—出动作"的闭环,而且不是平均值 31.25ms,是每一次都要在这个窗口内结束。这两者的差别在控制场景里是致命的:平均值达标、尾部抖动到 100ms 的系统,机械臂该抖还是会抖,抓取该掉还是会掉。
我把 31.25ms 切成几段来看会更清楚。相机曝光与传输大概 2 到 5ms(取决于曝光时间和传输接口),图像预处理在 CPU 上通常 1 到 3ms,视觉编码器前向 3 到 6ms,动作头加上采样步数 5 到 15ms,动作下发与通信 1 到 3ms,剩下的是框架调度和 Python 层面的固定开销。你会发现,真正留给"神经网络计算"的预算可能只有 15ms 左右,剩下的全被工程细节吃掉了。
所以当我看到 TurboVLA 宣称 32Hz 的时候,我关注的不是它用了多大的模型,而是它有没有把那些"看不见的开销"摁住。一个 0.2B 的模型在 RTX 4090 上做单次前向,理论算力根本不是瓶颈,瓶颈永远在别处。
1.2 参数小不等于跑得快,VLA 的瓶颈在别处
很多人有个直觉:参数少就等于快。这个直觉在半对半错之间。参数量决定了权重读取的带宽需求和计算量下限,但在小模型区间(0.1B 到 1B),单次前向的实际耗时往往被三件事主导:一是 kernel 启动开销与框架调度,二是序列长度带来的注意力计算,三是采样/解码的迭代次数。
第一条最容易被忽略。一次前向如果被拆成几百个小 kernel,每个 kernel 启动哪怕只要几微秒,累加起来也是几毫秒,而这几毫秒在 31.25ms 的预算里占掉了两成。第二条是 VLA 特有的痛点:视觉 token 数量往往远多于语言 token,224×224 分辨率、patch size 14 的 ViT,切出来就是 256 个 token,如果再拼上语言指令和状态向量,序列很容易冲到 400 以上。注意力是 O(n²) 的,序列一涨,延迟就非线性上涨。第三条则取决于动作头设计——如果是扩散策略或者流匹配,采样步数是直接乘在延迟上的。
把这三条摆出来,TurboVLA 的设计取向就基本能猜到了:视觉 token 要做压缩或池化,动作头要么步数极少要么干脆单步出,框架层面要尽量用 CUDA Graph 之类的手段把调度开销摊平。这三点也是我在自己项目里优化任何 VLA 时最先动刀的地方,顺序基本不会错。
1.3 一个经常被引用的反例
我见过不少团队在优化时先去换更小的骨干网络,把 3B 换成 1B,结果端到端只快了 15%。原因就是上面说的,瓶颈不在算力。反倒是把图像分辨率从 448 降到 224、把序列长度砍掉一半之后,延迟直接掉到原来的六成。这个顺序上的错误很常见,值得单独提一句:先测各段耗时占比,再决定优化对象,别凭直觉换模型。
2. VLA 推理耗时的拆解:一个控制周期里时间都去哪了
2.1 从相机曝光到动作下发的完整链路
把链路完整写出来,你会对"实时"有更具体的感受。典型的一条链路是这样的:触发相机采集 → 驱动层把图像拷到用户空间 → 格式转换与归一化 → 缩放与裁剪 → 转成张量 → 拷到 GPU → 视觉编码 → 与语言指令、本体状态拼接 → 动作头前向 → 反归一化 → 下发到执行器。这条链里,跨 PCIe 的拷贝、CPU 上的格式转换、以及没做异步处理的同步等待,是最容易累积延迟的三处。
我自己的习惯是给每一段都打上时间戳,跑一千个周期之后看 P50、P90、P99 三个分位数。只看平均值的优化等于没做优化,因为控制回路是对尾延迟敏感的。很多时候你会发现 P50 很漂亮,P99 却因为偶尔一次的垃圾回收或者内存分配涨到正常值的五倍。
下面这段代码是我常用的一个最小计时骨架,直接嵌在推理循环里,不引入额外依赖:
import time import torch @torch.inference_mode() def control_step(obs_img, state, instruction, preprocess, model): t0 = time.perf_counter() image = preprocess(obs_img) # CPU 侧,固定 dtype 与通道顺序 t1 = time.perf_counter() token = model.encode_vision(image) # 视觉编码 t2 = time.perf_counter() action_chunk = model.predict_actions(token, state, instruction) t3 = time.perf_counter() stats = { "preprocess_ms": (t1 - t0) * 1000, "vision_ms": (t2 - t1) * 1000, "action_ms": (t3 - t2) * 1000, "total_ms": (t3 - t0) * 1000, } return action_chunk, stats这段代码本身没什么技术含量,但它是所有后续优化的前提。没有分位数统计,你根本不知道自己在优化什么。
2.2 视觉编码器、动作头、采样步数这三块大头
视觉编码器的耗时和 token 数量几乎线性相关,而 token 数量是分辨率除以 patch size 再平方。224/14 得到 16×16=256,448/14 得到 32×32=1024,四倍差距。所以分辨率是第一个该动刀的地方,前提是任务本身不需要那么细的纹理信息——抓取大件物体和插拔细线缆对分辨率的要求完全不同。
动作头这块,目前的常见做法有两类。一类是把动作离散化成 token,用自回归的方式逐个吐出来,好处是能复用语言模型的解码逻辑,坏处是每一步都要过一次前向,动作维度一高就慢。另一类是连续动作的生成式方法,比如扩散策略或者流匹配,靠迭代去噪出动作,好处是动作平滑、多模态表达能力强,坏处是采样步数直接乘在延迟上。流匹配在 1 到 4 步就能出可用动作,这是它比传统扩散更适合实时场景的关键原因。
如果 TurboVLA 能在 32Hz 下工作,动作头的采样步数大概率被压到了个位数,甚至可能是单步。这一点对复现的人很重要:如果你照着某个开源实现默认的 10 步采样去跑,延迟会直接翻两三倍,然后你会误判成"这个模型根本跑不到实时"。
2.3 Python 侧开销:那些看不见的 10 毫秒
这一节是我最想强调的,因为它最反直觉。一个已经算得很干净的模型,端到端却跑不到 30Hz,八成是 Python 和框架层面在拖后腿。常见的元凶有这些:每步都新建张量导致的分配开销、.item()或.cpu()触发的隐式同步、日志打印、随机数生成、以及没开torch.inference_mode()导致自动求导图被偷偷构建。
.item()这一条尤其隐蔽。它本身只要几微秒,但它会把异步的 CUDA 队列强制同步,本来可以流水线重叠的计算被硬生生串行化,实测中造成 5 到 10ms 的额外延迟非常常见。同理,把张量搬到 CPU 再算损失或者做判断,也是同一个坑。
我在实际项目里做过一次对比,同一份推理代码,只做三件事——开inference_mode、把日志从每步输出改成每百步输出、把所有.item()从主循环里挪走——端到端延迟从 48ms 降到 31ms。模型一行没改。这个比例放在 32Hz 的语境下,就是"能跑"和"不能跑"的区别。
3. 0.9GB 显存账单怎么算出来的
3.1 权重、KV 缓存、激活值、上下文开销逐项拆
先做一个粗算,看看 0.9GB 这个数是否合理。0.2B 参数如果用 FP16 存权重,是 0.2×10⁹×2 字节 = 400MB,约 0.39GB。如果部分层用 INT8 或者 FP8,权重能压到 200MB 上下。这部分是死的,跑不掉。
KV 缓存是变量。假设模型 16 层、隐藏维度 768,那么每个 token 的 KV 缓存是 2×16×768×2 字节 ≈ 49KB。序列长度 512 的话就是 25MB;序列长度 2048 就是 100MB。对一个小模型来说,KV 缓存完全不是主要矛盾,这也解释了为什么在 0.2B 这个量级上,长上下文不会像在大模型上那样直接把显存吃爆。
激活值是另一块。前向过程中中间张量的峰值占用和 batch size、序列长度、隐藏维度都相关。做实时推理时 batch 通常就是 1,序列也不会太长,所以激活峰值一般在几十 MB 量级,除非视觉编码器那边分辨率开得很高。
真正容易被漏掉的是上下文开销。CUDA context、cuDNN/cuBLAS 的 workspace、PyTorch 的缓存分配器,这些加在一起在消费级卡上通常是 300 到 600MB。这个数字和模型完全无关,只要你初始化了 CUDA 就会占。所以一个"0.9GB"的读数里,可能有三分之一到一半根本不是模型本身。
3.2 显存数字的口径问题:nvidia-smi 与 torch 统计的差异
这是我特别想提醒的一点:不同工具给出的显存数字可以差出一倍,比较之前一定要先对齐口径。
| 统计方式 | 统计对象 | 典型差异来源 |
|---|---|---|
nvidia-smi | 进程占用的全部显存 | 包含 CUDA context、缓存分配器保留块、碎片 |
torch.cuda.memory_allocated() | 当前活跃张量 | 不含缓存分配器保留的空闲块 |
torch.cuda.memory_reserved() | 分配器向驱动申请的总量 | 通常大于 allocated,反映缓存水位 |
torch.cuda.max_memory_allocated() | 峰值活跃张量 | 最接近"模型真实需求"的口径 |
举个具体例子:一个模型用nvidia-smi看是 1.6GB,用max_memory_allocated看可能只有 0.85GB,中间那 0.75GB 是上下文开销加上分配器为了减少碎片而保留的空闲块。所以当你看到"0.9GB"这个数的时候,先问一句这是哪个口径的读数,否则拿它去和自己的 1.5GB 对比,结论会完全跑偏。
想干净地测一次,可以按这个顺序来:
import torch torch.cuda.empty_cache() torch.cuda.reset_peak_memory_stats() model = load_model(...) # 加载权重 _ = warmup(model) # 预热,触发 kernel 编译与缓存分配 start = torch.cuda.memory_allocated() out = model.infer(dummy_input) # 跑一次真实推理 print("权重与常驻:", (start) / 1024**3, "GB") print("峰值活跃 :", torch.cuda.max_memory_allocated() / 1024**3, "GB") print("分配器保留:", torch.cuda.max_memory_reserved() / 1024**3, "GB")预热这一步不能省。第一次前向会触发 kernel 自动调优、cuDNN 算法选择、以及各种惰性初始化,测出来的峰值会明显偏高。
3.3 想自己验证的话,几组值得记录的指标
如果打算长期维护这套推理链路,我建议把下面这几组指标做成常驻监控,它们能帮你在问题变严重之前发现苗头:
- 峰值显存(
max_memory_allocated):跨版本升级、换精度之后必看,能直接反映回归。 - 显存保留量与活跃量的差值:差值持续增大说明碎片在累积,长时间运行后可能 OOM。
- 单步延迟的 P50 / P90 / P99:只看平均会漏掉抖动问题。
- GPU 利用率与 SM 占用:利用率长期偏低说明瓶颈在 CPU 侧。
- 每秒推理次数与丢帧数:丢帧数是控制稳定性的直接指标。
顺便说一句,如果你怀疑显存本身有硬件问题(比如跑长任务时随机报错、或者同样的任务在不同机器上表现不一致),业界确实有一些底层显存检测工具可以做压力测试,思路是反复写入读出并校验。这类工具属于硬件排障范畴,和模型优化是两条线,别混在一起用。
4. 把 TurboVLA 跑起来:环境准备与最小推理脚本
4.1 依赖与版本对齐
小模型的部署,最大的坑往往不在模型,而在版本组合。我踩过的最典型的一次是:PyTorch 版本和 CUDA 版本匹配,但 flash attention 的实现和 PyTorch 版本不匹配,结果是能跑通,但速度只有应有的三分之一,而且精度还有细微偏差。这种问题不会报错,只会让你怀疑模型本身。
几条实操建议:一是把环境固定下来,用torch.__version__、torch.version.cuda、torch.cuda.get_device_capability()三个值记录基线,换机器时先比对;二是优先用官方推荐的组合,不要自己拼新版本;三是如果用到量化和编译加速组件,注意它们对 PyTorch 小版本通常非常敏感,锁死版本比追新重要得多。
4.2 权重加载与精度选择
在 RTX 4090 这类消费级卡上,精度选择有个实用的原则:**权重可以量化,注意力和归一化层尽量保持 FP16/BF16。**原因是动作输出对数值精度比较敏感,把注意力全部降到 INT8 有时会导致动作出现细微漂移,这种漂移在单次测试中看不出来,但会随着控制步数累积,表现出来就是机械臂末端慢慢偏移目标点。
另外要注意 BF16 和 FP16 的选择。4090 对两者都支持,BF16 动态范围更大、更不容易溢出,FP16 在某些 kernel 上速度略快。如果训练用的是 BF16,推理就别随手改成 FP16,数值分布的细微差异经过动作头放大之后,可能变成肉眼可见的行为差异。这一点在复现别人模型时特别重要。
4.3 一段可以直接改的最小推理循环
下面是我整理的一个最小可用推理循环,结构上刻意保持简单,方便你往里加东西:
import time import torch import collections torch.inference_mode().__enter__() torch.backends.cudnn.benchmark = True model = load_model(ckpt_path, dtype=torch.bfloat16, device="cuda") model.eval() # 用滑动窗口记录延迟,避免每步都做同步 lat = collections.deque(maxlen=200) action_queue = collections.deque() def loop(camera, robot, instruction, target_hz=32): period = 1.0 / target_hz next_tick = time.perf_counter() while robot.is_running(): now = time.perf_counter() if now < next_tick: time.sleep(max(0.0, next_tick - now)) next_tick += period img = camera.capture() # 建议走零拷贝路径 if len(action_queue) < 2: # 队列快空了才推理 t0 = time.perf_counter() chunk = model.predict_actions(img, robot.state(), instruction) lat.append((time.perf_counter() - t0) * 1000) action_queue.extend(chunk.tolist()) action = action_queue.popleft() robot.send(action, timestamp=time.perf_counter())循环里有三个设计点值得说。第一,time.sleep用于对齐节拍,但不要指望它精确,真实系统里更稳的做法是配合一个高精度定时器或者实时线程。第二,推理触发条件是"队列快空了"而不是"每帧都推理",这就是动作块(action chunking)的思想——一次推理出一小段动作,执行若干步之后再推下一次。第三,动作下发带时间戳,方便下游做延迟补偿。这三点在后面还会展开。
4.4 第一次跑通之后应该先测什么
很多人跑通第一个 demo 之后就急着上真机,结果在真机上遇到一堆没法定位的问题。我的建议是跑通之后按这个顺序测:先在固定输入下测延迟分位数,确认 P99 在预算内;再喂一段录制的回放数据,确认动作输出稳定、不抖;然后空跑机械臂执行动作,不接触任何物体,看轨迹是否平滑;最后才上真实任务。这个顺序能帮你把"模型问题"和"系统问题"分开,省掉大量排查时间。
5. 32Hz 稳定性靠什么守住:帧率抖动、动作队列与时间戳
5.1 平均帧率好看不代表控制稳
控制回路里的抖动(jitter)比平均延迟更值得关注。原因在于执行器是按固定节拍消费动作的,如果你的推理时不时慢一拍,执行器就会收到相邻两步之间间隔不均的动作序列,表现出来就是抖动或者过冲。32Hz 意味着每 31.25ms 一个动作点,如果其中某一次的延迟涨到 60ms,那一瞬间整个节奏就乱了。
实测中造成抖动的原因排个序,大概是:垃圾回收、显存分配与释放、操作系统调度、以及随机出现的后台任务。对付前两个的办法是预分配和对象复用——把输入输出张量提前开好,每步只做原地写入,避免分配器反复申请释放。对付后两个,则需要在进程优先级和线程模型上做隔离,把推理线程和日志、监控、网络通信这些杂活分开。
5.2 动作块与队列设计
动作块是解决实时性最有效的一招,道理很简单:把"每步都要推理"变成"每若干步推理一次",推理的延迟预算立刻宽松好几倍。假设一次推理出 8 个动作,以 32Hz 执行,那么推理本身只需要在 250ms 内完成就行,而不是 31.25ms。这个空间足够让模型跑得更从容,也足够让实现上不那么极限。
但动作块有它自己的代价。开环执行多步意味着模型看不到中间的状态变化,如果环境变化快或者模型预测偏保守,中间几步就可能出问题。常见的折中是:块长取 4 到 16,队列低于水位阈值时提前推理,同时保留一个"紧急重规划"的入口——检测到状态偏离阈值时立刻清空队列重新推理。这个阈值怎么定,得看具体任务容忍度,抓取大件物体可以放宽松,精细装配就得收紧。
5.3 时间戳对齐与丢弃策略
只要链路里存在延迟,动作下发的时候就必须带时间戳。原因有二:一是执行器可以用时间戳做插值,把不均匀的动作点映射到均匀的控制节拍上;二是当某个动作已经"过期"(超过容忍延迟才轮到执行)时,系统应该知道该丢弃它而不是硬着头皮执行。
过期动作判定的容忍度一般取一到两个控制周期,比如 32Hz 下就是 30 到 60ms。这个逻辑写进循环就是几行代码,但它能显著降低真机上偶发的"怪动作"——那些让人觉得"模型突然抽风了"的时刻,事后复盘往往发现是一个延迟很久的动作被延迟执行了。
6. 低显存部署踩坑记录:从精度漂移到显存碎片
6.1 动作反归一化统计量错位
这是 VLA 部署里最高频、也最难查的一类问题。模型训练时动作通常做了归一化,推理时需要拿训练集的统计量反归一化回去。如果加载的统计量和训练时的不一致(比如用错了数据集版本、或者分位数取的是 q01/q99 而不是 min/max),模型输出的动作数值范围会整体偏移,表现出来就是机械臂动作幅度不对或者明显偏向一侧。
排查方法很直接:喂一批训练时的样本,把模型输出的原始动作反归一化之后,和数据集里对应的真实动作做逐维对比。如果某一维的系统性偏差很大,八成是统计量的问题,而不是模型没训好。
6.2 图像通道与预处理的不一致
这个坑我中过不止一次。常见的图像读取库默认给出的是 BGR 顺序,而绝大多数模型是按 RGB 训练的;归一化时用的均值方差也可能不一致;缩放时用的插值方式(最近邻、双线性、双三次)差异在纹理丰富的场景下会真实影响输出。这些问题都不会报错,只会让模型表现"比预期差一点"。
我的做法是把预处理写成一份独立的、可测试的模块,给它固定输入然后逐像素比对参考实现。花半小时做这件事,能省掉后面好几天的"为什么效果不对"。
6.3 量化后的精度漂移定位
如果做了量化,一定要做逐层误差对比。思路是:拿若干真实输入,分别跑一次全精度和量化版本,记录每一层的输出差异。正常情况下误差会在一个很小的范围内波动,如果某一层突然放大,说明那一层不适合量化,把它单独保留高精度就行。
经验上,以下几类层比较容易出问题:第一层和最后一层、做归一化的层、以及任何涉及指数运算的层。把它们排除在量化范围外,整体显存增加不多,但输出稳定性会好很多。
6.4 显存碎片与长时间运行
短时间测试没问题、跑几个小时之后 OOM,这是碎片化的典型症状。PyTorch 的缓存分配器会尽量复用块,但如果你的序列长度、图像尺寸在不同时刻有变化,分配器就会保留下不同尺寸的块,长期下来保留量越来越大。
处理办法有几个:把输入尺寸和序列长度固定下来(动态形状是碎片的主要来源);周期性调用torch.cuda.empty_cache()(但别在主循环里调,它本身有开销);以及监控reserved和allocated的差值,差值持续增长就是要出问题的信号。
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 短时间正常,数小时后 OOM | 显存碎片累积 | 固定输入形状,监控 reserved 与 allocated 差值 |
| 换批次大小时延迟突增 | 重新触发 kernel 调优 | 固定形状并预热,或提前编译 |
| 输出动作整体偏移 | 反归一化统计量不匹配 | 用训练样本逐维比对原始输出 |
| 效果比预期差一点 | 通道顺序或归一化不一致 | 预处理模块化并逐像素比对 |
| 降精度后动作轻微漂移 | 敏感层被量化 | 逐层误差分析,敏感层保留 FP16 |
7. 微调与扩展:0.2B 还能再压榨出什么
7.1 LoRA 微调的显存预算
小模型微调在消费级卡上是完全可行的,但显存预算要算清楚。以 0.2B 为例,全参数微调时权重、梯度、优化器状态三份加起来,即使在混合精度下也要好几个 GB,还不算激活值。换成 LoRA 之后,可训练参数降到原来的百分之几,优化器状态和梯度都跟着大幅缩小,主要显存开销回到激活值和权重本身。
激活值这块靠梯度检查点(gradient checkpointing)来压,原理是用时间换空间——前向时不保存中间激活,反向时重新算一遍。这会增加大约三成的训练时间,但能把激活显存降下来一大截。训练显存吃紧的时候,先开梯度检查点,再考虑降 batch size,最后才动序列长度,这个顺序对收敛性的影响最小。
至于用不用现成的高效微调框架,我的看法是:如果你的目标是快速验证一个想法,用成熟框架省时间;如果要做深入调参和定制,自己写几十行训练循环反而更透明、更好调试。这两条路没有对错,取决于你要的是速度还是可控性。
7.2 多任务与多本体的取舍
0.2B 这个量级能不能承载多任务、多本体?我的观察是:能,但要克制。参数越少,任务之间的干扰越明显。如果硬塞进十几个差异很大的任务,模型很容易在每个任务上都表现平庸。比较务实的做法是先跑通一到三个高度相关的任务,把数据质量和动作空间对齐这两件事做扎实,再逐步扩展。
多本体(不同机械臂、不同自由度)的情况更复杂一些,通常需要给模型喂本体标识,或者对动作空间做统一映射。这里的取舍很实际:统一映射能让模型共享知识,但会引入映射误差;分开处理能保持精度,但每条线上都要单独维护数据和评估。我倾向于在小模型上先分开,等数据量足够再考虑统一。
7.3 什么时候该换更大的模型
这也是很多人关心的问题,尤其在显存充足的机器上,"既然卡够大为什么不用大模型"是很自然的想法。我的判断标准是看瓶颈在哪。如果失败案例集中在语义理解层面——比如听不懂复合指令、分不清相似物体、记不住多步任务的状态——那说明模型容量不够,换大模型有收益。但如果失败集中在执行层面——动作不平滑、抓取位置偏、反应慢——那换大模型基本没用,问题在数据和系统上。
现实里我见到的失败,执行层面的比例远高于语义层面。所以每次有人问"要不要换大模型",我的回答通常是:先把延迟分位数、动作平滑度、数据覆盖度这三个指标查一遍,再谈模型规模。
8. 实测之后的一点个人体会
折腾这类实时策略模型有一段时间了,最深的体会是:把一个大模型跑起来,和把一个小模型跑稳,是两种完全不同的能力。前者靠资源和耐心,后者靠对每一毫秒、每一兆字节的敏感度。TurboVLA 这组数字之所以有意思,不在于它把参数压到了多小,而在于它把一个完整的 VLA 链路压进了 31.25ms 和不到 1GB 的预算里——这背后必然是视觉 token 压缩、动作头步数控制、框架调度优化一整套组合拳,而不是单点突破。
如果你准备复现这套链路,我的建议是先把测量工具搭好再去优化。分位数计时、显存分口径统计、逐层精度对比,这三样东西花不了多少时间,但能让你少走很多弯路。另外,动作块、时间戳、过期丢弃这三个机制最好一开始就设计进去,它们是实时控制系统里最便宜也最有效的保险。
至于后续能扩展的方向,我比较看好两条:一是把量化做细,用逐层混合精度把显存再压一档同时不伤动作精度;二是把视觉 token 的压缩做成自适应的,简单场景少算、复杂场景多算,让平均延迟再降一些。这两条都不需要动模型结构,属于工程侧的纯收益,值得优先试。