SatNav 模型接入
本文介绍如何将新的导航模型接入 SatNav,并通过统一在线评测接口运行 Episode。接入层不
限制模型框架:PyTorch、Transformers、远程推理服务或规则策略都可以实现相同的
PolicyAdapter 接口。
开始前建议先阅读 Core API 和 Evaluation。前者介绍
observation、action 与 Env,后者介绍 Episode 选择、结果格式和聚合方式。
环境、策略和评测器的职责及交互闭环见系统全景。
1. 准备环境
在模型自己的 Python 环境中安装 SatNav Core:
python -m pip install -e .
python -c "from satnav.evaluation import Evaluator, PolicyAdapter; print('SatNav import OK')"
不同模型可以使用不同的 PyTorch、Transformers 或 CUDA 依赖。SatNav 只要求模型环境能够
导入 satnav;模型依赖不需要安装到 Core 环境中。
首次接入建议使用仓库自带的合成数据和场景:
applications/resources/
├── map.tif
├── satnav_example_episodes.json
└── satnav_example_task.yaml
这组资源不需要下载额外数据。接入代码能够在示例资源上完成 rollout 后,再切换到真实 Episode 和 GeoTIFF 场景。真实数据准备流程参阅 Episode 数据下载和 卫星场景下载。
2. 接入接口
一次评测的调用关系如下:
Episode
→ Env.reset_to_episode()
→ observation
→ PolicyAdapter.act()
→ PolicyStep
→ Env.step()
→ metrics and result
模型只需实现三个方法:
class PolicyAdapter:
def reset(self, context): ...
def act(self, observation): ...
def close(self): ...
方法 |
调用时机 |
责任 |
|---|---|---|
|
每个 Episode 开始时 |
清空历史状态并读取当前 Episode 信息 |
|
每个环境 step 前 |
返回一个 SatNav primitive action |
|
整个 worker 结束时 |
释放模型、文件句柄或外部服务连接 |
PolicyAdapter 是 Python Protocol,不要求继承某个基类。只要对象提供这三个方法,就可以
传给 Evaluator。
3. 最小 Adapter
下面的 adapter 在每个 Episode 中前进三步,然后执行 STOP。它不加载模型,但完整演示
了 Episode 状态重置、动作返回和资源释放。
from typing import Any, Mapping, Optional
from satnav.evaluation import EpisodeContext, PolicyStep
from satnav.task.actions import Action
class ForwardThenStopAdapter:
def __init__(self, forward_steps: int = 3) -> None:
self.forward_steps = int(forward_steps)
if self.forward_steps < 0:
raise ValueError("forward_steps must be non-negative")
self._step = 0
self._episode_key: Optional[str] = None
def reset(self, context: EpisodeContext) -> None:
self._step = 0
self._episode_key = context.episode_key
def act(self, observation: Mapping[str, Any]) -> PolicyStep:
del observation
if self._episode_key is None:
raise RuntimeError("reset() must be called before act()")
if self._step >= self.forward_steps:
action = Action.STOP
else:
action = Action.MOVE_FORWARD
self._step += 1
return PolicyStep(
action=action,
info={"adapter_step": self._step},
)
def close(self) -> None:
self._step = 0
self._episode_key = None
同一个 adapter 会连续处理多个 Episode,因此所有
Episode 级状态都应在 reset() 中重新初始化。
4. EpisodeContext
reset() 接收的 EpisodeContext 提供当前 Episode 和评测进程信息:
字段 |
说明 |
|---|---|
|
当前 |
|
|
|
当前数据划分 |
|
当前 Episode 在全局选择结果中的位置 |
|
当前 worker 和总 worker 数 |
|
当前 Episode 最大动作数 |
|
当前 Episode 的确定性随机种子 |
|
当前公开 |
|
|
|
|
需要随机采样时,应在每次 reset() 中使用 context.seed 初始化 Episode 级随机数生成器。
这样同一 Episode 在不同 rank 数量或 resume 边界下仍能得到相同随机序列。
Adapter 可以读取 context.episode.instruction、reference_path 或 trajectory_type,也可以
通过 context.agent_state 获取当前位置。不要访问 Env 的 _dataset、_task 或 _sim
等私有属性。
5. Observation 与 Action
默认 VLNTask 返回:
Key |
类型 |
说明 |
|---|---|---|
|
|
|
|
|
|
|
|
|
RGB 尺寸由 task config 决定。预处理代码应读取输入的实际 shape,不要固定为 224 或 448。 完整 observation 定义参阅 Core API - Observation。
SatNav action 为:
ID |
Action |
|---|---|
0 |
|
1 |
|
2 |
|
3 |
|
PolicyStep.action 可以使用动作字符串或整数 ID。建议 adapter 在返回前完成模型输出到
SatNav action 的显式映射:
from satnav.task.actions import Action
MODEL_ACTIONS = {
"stop": Action.STOP,
"forward": Action.MOVE_FORWARD,
"left": Action.TURN_LEFT,
"right": Action.TURN_RIGHT,
}
def decode_action(model_output: str) -> str:
key = model_output.strip().lower()
if key not in MODEL_ACTIONS:
raise ValueError(f"unsupported model action: {model_output!r}")
return MODEL_ACTIONS[key]
不要将无法解析的模型输出静默转换为 STOP,否则会把模型格式错误记录成正常终止并影响
评测指标。
PolicyStep.info 可保存生成文本、置信度、action queue 长度等诊断信息。这里的内容会在
启用 action trace 时写入 JSONL,因此应满足以下要求:
可以序列化为 JSON;
不包含完整 RGB、模型 tensor 或其他大对象;
不包含 checkpoint、数据集或场景的本机绝对路径;
不包含 token、凭据或远程服务请求头。
6. 运行最小评测
将第 3 节的 ForwardThenStopAdapter 与内置示例环境连接:
import json
from pathlib import Path
from applications.resources import load_example_task_config
from satnav.core.env import Env
from satnav.evaluation import EvaluationConfig, Evaluator
config = load_example_task_config()
environment = Env(config, cycle=False)
policy = ForwardThenStopAdapter(forward_steps=3)
summary = Evaluator(
environment=environment,
policy=policy,
config=EvaluationConfig(
output_dir=Path("output/model_integration/forward_then_stop"),
split=str(config.DATASET.SPLIT),
policy_id="forward-then-stop",
limit=2,
max_steps=10,
fail_on_episode_error=True,
),
).run()
print(json.dumps(summary, indent=2, sort_keys=True))
Evaluator.run() 会在成功或异常结束时关闭 policy 和 environment。运行结果位于:
output/model_integration/forward_then_stop/
├── rank_00000/
│ ├── episodes.jsonl
│ └── done.json
└── summary.json
首先检查 summary.json 中的 status 和 error_episode_count,再查看 JSONL 中单个 Episode
的 action trace 与 metrics。结果字段的完整定义参阅 Evaluation - 输出格式。
7. 接入真实模型
真实模型通常在 adapter 创建时加载一次,在多个 Episode 之间复用。只有 recurrent state、
历史图像、prompt 和待执行 action queue 等 Episode 级状态需要在 reset() 中清空。
from typing import Any, List, Mapping, Optional
from satnav.evaluation import EpisodeContext, PolicyStep
class MyModelAdapter:
def __init__(self, model: Any, processor: Any, device: str) -> None:
self.model = model
self.processor = processor
self.device = device
self._context: Optional[EpisodeContext] = None
self._history: List[Any] = []
self._action_queue: List[str] = []
self._closed = False
@classmethod
def from_pretrained(cls, model_path: str, device: str) -> "MyModelAdapter":
# 在这里导入模型框架并加载 checkpoint。
model, processor = load_your_model(model_path, device=device)
model.eval()
return cls(model=model, processor=processor, device=device)
def reset(self, context: EpisodeContext) -> None:
if self._closed:
raise RuntimeError("adapter is closed")
self._context = context
self._history.clear()
self._action_queue.clear()
reset_model_state(self.model)
def act(self, observation: Mapping[str, Any]) -> PolicyStep:
if self._context is None:
raise RuntimeError("reset() must be called before act()")
if self._action_queue:
action = self._action_queue.pop(0)
source = "queued"
else:
rgb = observation["rgb"]
instruction = observation["instruction"]["text"]
model_input = self.processor(
image=rgb,
text=instruction,
history=self._history,
)
prediction = run_model(self.model, model_input, device=self.device)
action, remaining = parse_model_actions(prediction)
self._action_queue.extend(remaining)
source = "generated"
self._history.append(observation["rgb"])
return PolicyStep(
action=action,
info={
"source": source,
"queue_remaining": len(self._action_queue),
},
)
def close(self) -> None:
if self._closed:
return
self._context = None
self._history.clear()
self._action_queue.clear()
self.model = None
self.processor = None
self._closed = True
示例中的 load_your_model()、reset_model_state()、run_model() 和
parse_model_actions() 由具体模型实现。接入时应重点确认:
checkpoint 加载完整,模型进入 evaluation mode;
推理过程关闭梯度计算;
RGB 的颜色顺序、尺寸和归一化与模型训练时一致;
instruction 使用当前 Episode,而不是上一个 Episode 的缓存;
recurrent/KV cache、历史帧和 action queue 在每次
reset()中清空;action chunk 按 primitive action 逐步返回,不在一次
act()中直接执行多个环境 step;close()可以重复调用。
8. 项目组织
模型可以保存在独立仓库中,只依赖 SatNav 的公开接口:
my_satnav_model/
├── pyproject.toml
├── my_satnav_model/
│ ├── adapter.py
│ └── evaluate.py
├── configs/
│ └── satnav_task.yaml
└── README.md
如果模型作为 SatNav 仓库内维护的 baseline,建议使用:
baselines/vlm/<model_name>/
├── adapter.py
├── evaluate.py
├── configs/
│ └── satnav_task.yaml
├── scripts/
│ └── eval.sh
├── requirements.txt
├── local.env.example
└── README.md
公开配置只保存相对路径和模型无关参数。checkpoint、Episode、GeoTIFF、上游源码路径和
输出目录应通过 CLI、环境变量或 Git ignored 的 .local/env.sh 提供。
模型依赖应在 adapter.py 或模型 factory 内延迟导入。仅执行
import satnav 或 import satnav.evaluation 时,不应加载 PyTorch、Transformers 或模型
checkpoint。
9. 评测入口
一个模型评测入口通常按以下顺序工作:
解析模型、Episode、场景和输出路径;
加载 task config,并设置 split、
DATASET.DATA_PATH和DATASET.SCENES_DIR;创建
SatNavDataset和Env(cycle=False);加载模型并创建 adapter;
构造
EvaluationConfig;调用
Evaluator.run();输出
summary.json中的汇总结果。
建议为所有模型提供以下公共参数:
参数 |
作用 |
|---|---|
|
checkpoint 或模型目录 |
|
SatNav task config |
|
当前 split 的 Episode JSON |
|
GeoTIFF 场景目录 |
|
|
|
评测输出目录 |
|
Episode 选择范围 |
|
多进程 Episode 分片 |
|
评测随机种子 |
|
每个 Episode 最大动作数 |
|
跳过当前 rank 已完成的 Episode |
|
任一 Episode 失败时令最终命令失败 |
|
只检查配置和 Episode 选择,不加载模型 |
可以参考现有入口的组织方式:
baselines/classic/evaluate.py:Classic factory 与统一 evaluator;baselines/vlm/navila/evaluate.py:外部模型、独立环境和 GPU 选择;baselines/vlm/openfly/evaluate.py:本地 checkpoint 与 processor 加载。
--dry-run 应在模型加载前结束,使开发者可以先检查 Episode 数、rank shard 和输出目录。
10. 多 rank 评测
在线评测使用 Episode 分片,每个 rank 独立加载模型并运行自己的 Episode。模型不需要通过 DDP 包装,但所有 rank 必须使用相同的数据、split、选择参数、seed、最大步数和输出目录。
下面是两张 GPU 的启动示例;my_satnav_model.evaluate 表示模型自己的 CLI:
CUDA_VISIBLE_DEVICES=0 python -m my_satnav_model.evaluate \
--rank 0 --world-size 2 --device cuda:0 \
--output-dir output/my_model/val_seen &
pid0=$!
CUDA_VISIBLE_DEVICES=1 python -m my_satnav_model.evaluate \
--rank 1 --world-size 2 --device cuda:0 \
--output-dir output/my_model/val_seen &
pid1=$!
wait "$pid0"
wait "$pid1"
python scripts/evaluation/aggregate.py output/my_model/val_seen \
--fail-on-episode-error
limit 先应用于全局排序结果,再进行 stride sharding。例如 limit=8、world_size=2 时,
两个 rank 各处理四个 Episode。完整规则参阅 Evaluation - Episode 选择与分片。
11. Resume 与错误处理
同一运行中断后,可以使用相同参数和输出目录重新启动,并设置 resume=True 或 CLI 的
--resume。Evaluator 会跳过当前 rank JSONL 中已有的 episode_key。
只有模型、数据、split、rank 数量、seed 和评测参数保持一致时才应 resume。更改运行条件 时应使用新的输出目录。
调试新 adapter 时建议同时启用:
fail_fast=True
fail_on_episode_error=True
capture_action_trace=True
fail_fast 在记录首个失败 Episode 后停止当前 worker;fail_on_episode_error 让最终命令
在存在失败记录时返回非零状态;action trace 用于查看模型输出经过 adapter 后实际执行的
primitive action。
大规模正式评测确认接入稳定后,可以设置 capture_action_trace=False 减少结果文件体积。
12. 常见问题
模型应该从哪里读取 instruction?
可以读取 observation["instruction"]["text"],也可以在 reset() 中读取
context.episode.instruction.instruction_text。前者适合逐 step 统一预处理,后者适合构建
Episode 级 prompt。
模型一次生成多个动作怎么办?
将生成结果解析为 action queue。当前 act() 返回第一个 primitive action,后续调用依次
返回剩余动作;每次 Env.step() 后仍应更新历史 observation 和模型状态。
必须把模型代码放进 SatNav 仓库吗?
不需要。外部包只要安装 SatNav 并实现 PolicyAdapter 即可。只有希望由 SatNav 仓库直接
维护和发布的 baseline,才需要放入 baselines/。
Adapter 可以直接控制 simulator 吗?
通常不需要。Adapter 应返回 action,由 Evaluator 调用 Env.step(),这样 Episode 终止、
metrics 和 action trace 才会同步更新。只在 navigation helper 需要读取底层状态时使用
context.simulator 的公开接口。
为什么模型已经输出 STOP,但 Success 仍为 0?
STOP 只表示模型决定结束 Episode。只有执行 STOP 时 agent 位于当前任务类型的成功半径
内,SUCCESS 才为 1。标准阈值参阅 Evaluation。