|
Download satlink/API.md from lulidong/rs: direct link, hf CLI and curl.
- Browser
- Download file 17 kB
-
https://huggingface.co/lulidong/rs/resolve/main/satlink/API.md
- Command line
-
hf download hf://lulidong/rs/satlink/API.md
-
curl -L -o API.md https://huggingface.co/lulidong/rs/resolve/main/satlink/API.md
17 kB
| # satlink 调用接口 | |
| 面向外部应用的公开接口。三个对象覆盖整条链路: | |
| | 对象 | 作用 | 规模 | 在哪一侧 | | |
| |---|---|---|---| | |
| | `satlink.Satellite` | 像素 → 码字 | 5.9M 参数 | 星上 | | |
| | `satlink.Ground` | 码字 → 像素 | 337.8M 参数 | 地面 | | |
| | `satlink.GroundVLM` | 码字 → 答案 | 解码器 + 视觉塔 + LLM,一个整体 | 地面 | | |
| 支持两个 tokenizer 后端,由权重文件自动识别,不用传标志: | |
| | 后端 | 权重 | 每片 | 单片下行 | 压缩比 | PSNR | 通路 | | |
| |---|---|---|---|---|---|---| | |
| | `opd` One-D-Piece | `sat_encoder.pt` / `ground_decoder.pt` | 48 tok × 12 bit | 72 B | ~910× | ~21 dB | 像素 + 隐空间 | | |
| | `rstok` RS-Tokenizer | `rstok_sat.pt` / `rstok_ground.pt` | 256 tok × 18 bit | 576 B | ~114× | ~41 dB | **仅像素** | | |
| 多 8 倍带宽换约 20 dB。选哪个取决于链路和画质哪个更紧张。`rstok` 只接了像素通路—— | |
| 隐空间通路的 bridge 是针对 One-D-Piece 那个特定的中间层特征训出来的,换 tokenizer 能跑但是错的。 | |
| 两个值类型在它们之间传递:`satlink.VQFrame`(码字 + 摆放几何)和 `satlink.LinkProfile` | |
| (帧长宽 / 切片大小 / 每片 token 数——星地**约定好的常量,不上下行**)。 | |
| `import satlink` 本身不加载 torch,也不加载任何模型;`Satellite` / `Ground` / `GroundVLM` | |
| 在**首次访问时**才解析。这不是为了好看:星上进程必须能 import 本包而不把 337.8M 参数的解码器 | |
| 拉进内存,这一点由 `satlink/bench/test_isolation.py` 静态校验。 | |
| --- | |
| ## 1. 星上 | |
| ```python | |
| import satlink | |
| sat = satlink.Satellite( | |
| "satlink/weights/sat_encoder.pt", | |
| 32768, 30720, # 部署固定的帧尺寸 | |
| n_tok=48, # 每个 256x256 切片取多少 token | |
| dtype="float16", # float16 / bfloat16 / float32 | |
| device="auto", # auto 会优先选 musa(M1000),其次 cuda,最后 cpu | |
| ) | |
| frame = sat.encode_file("scene.png", batch=64) # PNG / BMP 都支持,任意尺寸 | |
| wire = sat.to_wire(frame) # 下行字节流,无包头 | |
| sat.save(frame, "codes.json") # 落盘(原子写入) | |
| ``` | |
| | 成员 | 说明 | | |
| |---|---| | |
| | `sat.encode_file(path, batch=64)` | 流式读 PNG/BMP,一次一条 256 像素高的横条,峰值内存是一条带而不是整幅 | | |
| | `sat.encode(image, batch=64)` | 已在内存里的 PIL 图 / `HxW`、`HxWx3` 数组 / `CHW` 张量 | | |
| | `sat.to_wire(frame)` | 打包成下行字节 | | |
| | `sat.save(frame, path, **extra)` | 写 JSON,原子替换 | | |
| | `sat.n_crops` / `sat.frame_bytes` / `sat.compression_ratio` | 切片数 / 单帧下行字节 / 相对 8bit 灰度原图的压缩比 | | |
| **关于 `batch`。** 它是一次推给编码器的切片数,用来在显存和吞吐之间取舍,可以随便调。 | |
| 但它在数值上不是中性的:不同 batch 会选中不同的 GEMM kernel,其舍入差异会让**落在量化边界 | |
| 附近**的码字翻转,实测约 0.2%–0.5%。平均重建影响 < 0.4/255,但局部单点最大可达 ~118/255。 | |
| 结论是:不同 batch 的两次运行**不保证逐位相同**,验收判据应当看重建像素,而不是码字。 | |
| **关于精度(重要,两个后端的约束相反)。** | |
| | | 编码器 | 解码器 | | |
| |---|---|---| | |
| | `opd` | fp16 / bf16 / fp32 均可 | fp16 / bf16 / fp32 均可(默认 bf16) | | |
| | `rstok` | **不能 bf16**(用 fp16) | **不能 fp16**(用 bf16,默认) | | |
| `rstok` 编码器在 bf16 下,8 张参考图里有 1 张从 41.4 dB 塌到 22.5 dB——19 万码字在 24 维单位球面上 | |
| 近邻的余弦间隔极小,bf16 的 8 位尾数分不开,argmin 会整块选错。fp16 全程与 fp32 差 <0.5 dB。 | |
| `rstok` 解码器反过来:1024 宽 24 层,激活会冲出 fp16 的动态范围,**整帧变成 NaN**, | |
| 然后被 cast 成一张全黑图。 | |
| **所以解码器统一走 bf16**——实测两个后端的 bf16 与 fp32 差 0.00 dB,显存减半,也绕开了上面这个坑。 | |
| `Ground` 和 `run_ground` 的默认值都是 `bfloat16`。保留两道护栏:`RSTokDecoder` 构造时拒绝 fp16, | |
| `Ground.decode` 检查输出是否有限——非有限直接报错,而不是输出一张看起来「成功」的黑图。 | |
| (官方参考输出是 bf16 跑的,所以继承了编码器那个塌陷。`test_rstok.py` 把精确匹配钉在 bf16 上, | |
| 那是**溯源检查**,不是推荐用 bf16。) | |
| **关于图像格式。** PNG 和 BMP 都支持,且**结果逐位一致**(已验证)。两者读法不同: | |
| BMP 未压缩、行有 4 字节对齐填充,可以 `np.memmap` 按需读某一条;PNG 是单条 zlib 流、 | |
| 行间有 filter 依赖,无法定位到第 N 行,只能整幅解码一次再切。接口一样,代价不一样。 | |
| 读取器返回**原生通道数**,不自己转灰度——转灰度用的是 `satlink/sat/encoder.py` 里的 | |
| float BT.601,属于模型契约的一部分;若在读取阶段用 PIL 的定点灰度替代,像素会差 1 LSB, | |
| 足以翻转码字,导致「流式编码」和「整幅编码」结果不一致。 | |
| --- | |
| ## 2. 地面:重建 | |
| ```python | |
| import satlink | |
| frame = satlink.load_codes("codes.json") # 或 satlink.frame_from_wire(wire, 32768, 30720) | |
| g = satlink.Ground("satlink/weights/ground_decoder.pt", dtype="float32") | |
| img = g.reconstruct(frame) # PIL 灰度图 | |
| img.save("recon.png") | |
| x = g.decode(frame) # 想要张量就用这个:[H, W],float,[0,1],重叠区已平均 | |
| ``` | |
| ## 3. 地面:直接问模型 | |
| ```python | |
| vlm = satlink.GroundVLM("model/RSThinker", | |
| decoder="satlink/weights/ground_decoder.pt") | |
| print(vlm.ask(frame, "where is the ship")) | |
| ``` | |
| **像素路径的模型是懒加载的。** 它的解码器要绑定帧几何,而构造时还不知道,所以 | |
| `GroundVLM(...)` 会立刻返回、**完全不碰 GPU**,模型在第一次 `ask()` 时才上卡。 | |
| 盯着 nvidia-smi 的人会觉得没加载。想立即加载就传 `profile`: | |
| ```python | |
| vlm = satlink.GroundVLM("model/RSThinker", | |
| decoder="satlink/weights/ground_decoder.pt", | |
| profile=satlink.LinkProfile(512, 512)) # 构造时就上卡 | |
| ``` | |
| 隐空间路径没有这个绑定,一直是构造时就加载。 | |
| **合并检查点(推荐)。** 像素路径要读基座 + 一个单独的 338M 解码器 `.pt`;隐空间路径每次还要 | |
| 把 240 个 LoRA 权重折进基座。这些都不依赖运行时才知道的东西,所以预先合并成一份普通的 | |
| safetensors,加载时什么都不用合: | |
| ```python | |
| vlm = satlink.GroundVLM("model/RSThinker-satlink-pixel") # 不用传 decoder= | |
| vlm = satlink.GroundVLM("model/RSThinker-satlink-latent") # 不用传 latent_ground=/lora= | |
| ``` | |
| 合并检查点带一个 `satlink.json` 标记自己是哪条路,接口据此自动选。这时再传 | |
| `decoder=`/`latent_ground=`/`lora=` 会直接报错——不然到底用哪个解码器是有歧义的。 | |
| 自己合并: | |
| ```bash | |
| python -m satlink.tools.export_merged --path pixel --model model/RSThinker \ | |
| --decoder satlink/weights/ground_decoder.pt --out model/RSThinker-satlink-pixel | |
| python -m satlink.tools.export_merged --path latent --model model/RSThinker \ | |
| --ground satlink/weights/latent_ground_dec12.pt \ | |
| --lora outputs/latent_sft_dec12/checkpoint-2241 --out model/RSThinker-satlink-latent | |
| ``` | |
| | 加载方式 | 耗时 | 权重 | | |
| |---|---|---| | |
| | 像素 分离式 | 9.4 s | — | | |
| | 像素 合并式 | **7.3 s** | 与分离式**逐张完全一致** | | |
| | 隐空间 分离式 + LoRA | 15.9 s | — | | |
| | 隐空间 合并式 | **6.9 s**(2.3×) | 与分离式**逐张完全一致** | | |
| **加载开销**(10.63B 模型,A100): | |
| | | 显存 | 峰值主机内存 | 耗时 | | |
| |---|---|---|---| | |
| | `GroundVLM(...)` 不带 profile | 0 | ~0 | 0.3 s | | |
| | 首次 `ask()` / 带 profile 构造 | 21.3 GB | 5.6 GB | ~10–15 s | | |
| 两条路径二选一,不能都给: | |
| ```python | |
| # 像素路径:码字在模型内部解成像素再嵌入 | |
| satlink.GroundVLM(model_dir, decoder="satlink/weights/ground_decoder.pt") | |
| # 隐空间路径:只做部分解码,直接投影进视觉塔的 patch-embedding 空间,全程不产生像素 | |
| satlink.GroundVLM(model_dir, | |
| latent_ground="satlink/weights/latent_ground_dec12.pt", | |
| lora="outputs/latent_sft_dec12/checkpoint-2241") | |
| ``` | |
| --- | |
| ## 4. 协议层 | |
| 链路上流动的只有码字:每个 256×256 切片 48 个索引 × 12 bit = **72 字节**,相对灰度原图约 910×。 | |
| **没有包头**——帧尺寸、切片大小、每片 token 数、码本位宽都是两端已经约定的常量, | |
| 花下行字节去传一个永远相同的头是纯开销。切片序号和帧边界由**流中的位置**隐含决定。 | |
| ```python | |
| from satlink import LinkProfile, pack_frame, unpack_frame | |
| p = LinkProfile(32768, 30720, tile=256, n_tok=48, bits=12) | |
| p.grid # (128, 120) —— ceil(W/256) x ceil(H/256) | |
| p.n_tiles # 15360 | |
| p.crop_bytes # 72 | |
| p.frame_bytes # 1105920 (1.05 MiB) | |
| p.tile_origin(i) # 第 i 个切片的左上角 | |
| ``` | |
| 切片是**重叠**的:最后一行/列与边缘齐平,而不是把余量补边。补边会让 tokenizer 把预算花在 | |
| 编码一条复制出来的条带上,而那条带的重建会渗进真实像素里。边长是 256 整数倍时重叠为零 | |
| (32768×30720 正是如此,所以能拿到完整的 910×);不是整数倍时会有冗余,比如 600×600 被 | |
| 3×3 切片覆盖了 768 像素的内容,多花 1.64× 的 token,压缩比相应降到 555×。 | |
| --- | |
| ## 5. 设备 | |
| ```python | |
| from satlink import resolve_device | |
| resolve_device("auto") # 'musa' (M1000, 需要 torch_musa) / 'cuda' / 'cpu' | |
| ``` | |
| `device="auto"` 优先选 MUSA。显式传 `device="musa"` 而 `torch_musa` 没能 import 时会直接报错, | |
| 不会静默退回 CPU——验收场景下静默降级比失败更糟。 | |
| --- | |
| ## 6. 全流程脚本 | |
| 一个驱动跑通全链路,星上阶段可以单独跑并落盘,地面阶段只从落盘产物恢复。 | |
| ```bash | |
| # 全流程 | |
| python -m satlink.examples.pipeline --image scene.png --out run1 | |
| # 只跑星上,下行产物全部落盘 | |
| python -m satlink.examples.pipeline --image scene.png --out run1 --stage sat --dump-crops | |
| # 只跑地面 —— 不碰原图,也不加载星上模型 | |
| python -m satlink.examples.pipeline --out run1 --stage ground \ | |
| --model model/RSThinker --question "where is the ship" | |
| # 换 RS-Tokenizer 后端 | |
| python -m satlink.examples.pipeline --image scene.bmp --out run2 \ | |
| --sat-weights satlink/weights/rstok_sat.pt \ | |
| --decoder satlink/weights/rstok_ground.pt | |
| ``` | |
| 落盘内容: | |
| | 文件 | 内容 | | |
| |---|---| | |
| | `manifest.json` | profile / 后端 / 精度 / 各段耗时 / 字节数 / 压缩比 / 地面结果 | | |
| | `codes.json` | 可读形式的码字 | | |
| | `downlink.json` | 打包后的下行比特流,十六进制字符串放在 JSON 里——电台真正发的东西,但能用文本编辑器打开 | | |
| | `crops/` | `--dump-crops` 时,编码器实际看到的每张 256×256 灰度图 | | |
| | `recon.png` / `compare.png` / `answer.txt` | 地面产物 | | |
| 两处是刻意的: | |
| - **`--stage ground` 不接受 `--image`。** 地面能不能只靠下行数据还原,是这套拆分成立与否的判据; | |
| 它要是偷看原图,测的就不是链路了。 | |
| - **地面会先核对 `downlink.json` 和 `codes.json` 解出的码字是否一致**,不一致就拒绝继续。 | |
| 两者是同一份数据的两种形态,只有一条错比两条都错更难发现。 | |
| 实测(1024×768,同一张图): | |
| ``` | |
| opd 12 片 x 48 tok x 12 b = 864 B 910.2x 20.80 dB | |
| rstok 12 片 x 256 tok x 18 b = 6912 B 113.8x 27.56 dB | |
| ``` | |
| --- | |
| ## 7. 内置示例 | |
| `satlink/assets/showcase/` 里有 11 张 VRSBench 影像,以及模型**从 satlink 码字**给出的回答, | |
| VQA 和 grounding 都有。装完不接任何评测数据就能验证这套东西是通的。 | |
| ``` | |
| satlink/assets/showcase/ | |
| showcase.json 11 个例子的问题、标准答案、模型回答、IoU、各后端重建指标 | |
| images/ 原图 11 张 | |
| recon/ 每张图两个后端的重建,共 22 张 | |
| ``` | |
| | | | | |
| |---|---| | |
| | VQA | 31 题,覆盖 8 种题型(数量/位置/类别/存在/颜色/尺寸/方向/场景) | | |
| | Grounding | 24 题,平均 IoU 0.684,最高 1.000,最低 0.519 | | |
| | 重建 | opd 平均 19.4 dB @ 910×;rstok 平均 25.1 dB @ 114× | | |
| **只收录答对的题**——按 VRSBench 标准答案判定,grounding 要 IoU≥0.5,VQA 用 `vqa_ok`。 | |
| 参考答案要是错的,读的人就分不清「真的退化了」和「基线本来就差」,而且那些数字会被当成模型准确率引用。 | |
| 这是**演示集,不是评测集**:11 张图量不出准确率,那是 `satlink/eval/` 在几千条上做的事。 | |
| ```bash | |
| python -m satlink.bench.test_showcase # 只查重建,不需要 VLM | |
| python -m satlink.bench.test_showcase --backend both | |
| python -m satlink.bench.test_showcase --model model/RSThinker # 再把 55 题全问一遍 | |
| ``` | |
| 重建按 **PSNR 与记录值的偏差**判定,不按码字。码字是大码本上的 argmin,换 GPU、换精度、换 batch | |
| 都会翻一部分,而图几乎不动;钉码字会让这个测试在除了打包那台机器以外的所有机器上失败。 | |
| 关于 VLM 那一腿有个实测结论值得记着:**回答在重新分批后不是逐字可复现的**。同样的图、同样的 | |
| 提示词、temperature=0,batch 组成一变,数值路径就变,350 token 的推理链里早期翻一个 token | |
| 就足以改掉最终答案——实测 61 题里 12 题措辞不同、6 题结论翻转。所以参考答案是用 | |
| `--rerecord-batch 1` 从**打包好的那张图**重录的,测试也默认 batch=1。当前状态:55/55 仍判正确, | |
| 54/55 逐字相同。 | |
| --- | |
| ## 8. 验收脚本 | |
| 星地两侧分开跑,因为星上那台机器**没有解码器也没有 LLM**——这正是拆分的意义。 | |
| 地面跑完用 `--append-to` 把重建和 LLM 结果并回该用例的报告,做到**一个用例一份报告**。 | |
| ```bash | |
| # 用例1 · 检测网络性能测试(F01 启动与离线读取 / F02 离散Token压缩 / F03 结果输出与存储) | |
| python -m satlink.accept.run_sat --case 1 --data /mnt/ssd/testdata1 \ | |
| --out results/case1 --dtype float16 --batch 64 | |
| # 用例2 · 软件系统功能测试(另加 F03 ≥5min 连续循环 / F04 资源与功耗监控 / F05 输出与存储) | |
| python -m satlink.accept.run_sat --case 2 --data /mnt/ssd/testdata2 \ | |
| --out results/case2 --min-seconds 300 --dtype float16 --batch 64 | |
| # 地面:重建 + LLM,并合并回对应用例的报告 | |
| python -m satlink.accept.run_ground --codes results/case1/codes \ | |
| --out results/case1/ground --source /mnt/ssd/testdata1 \ | |
| --model model/RSThinker --append-to results/case1/report.json | |
| ``` | |
| 每条判据都在代码里判定并写进 `report.json`,只有重建的**视觉检查**留给人看 | |
| (图在 `results/*/ground/recon/`)。 | |
| 几点是刻意这么做的: | |
| - **功耗采不到时判为失败,不判为通过。** 从 `/sys/class/hwmon/hwmonN/{in1_input,curr1_input}` | |
| (mV × mA)1 Hz 采样,判据平均 ≤77.5 W、峰值 ≤90.5 W。传感器缺失时报告会写明「未能采集」 | |
| 并判失败——验收报告把「没测到」记成「合格」,比测出不合格更危险。 | |
| - **故意塞一个损坏文件。** F01 会自己构造一个坏 PNG,要求读取器**明确报错**; | |
| 若被当成可用输入而静默跳过,该步判失败。这防的是「伪成功」。 | |
| - **输出是原子写入。** 先写 `.tmp` 再 `os.replace`,读者要么看到旧文件、要么看到完整新文件, | |
| 不会读到写了一半的结果。报告里会检查有没有残留 `.tmp`。 | |
| - **压缩比判据与帧尺寸有关。** 失败时报告会算出重叠冗余系数并给出「若边长为 256 整数倍则为 | |
| 多少」,免得看到一个数字却不知道为什么不达标。 | |
| ### 已验证的行为 | |
| 在 A100 上以 1024×768(256 的整数倍)实测: | |
| | 项 | 结果 | | |
| |---|---| | |
| | 压缩比 | 910.2×(判据 ≥900) | | |
| | PNG 与 BMP 编码结果 | 逐位一致 | | |
| | PNG 与 BMP 重建 PSNR | 均为 20.80 dB | | |
| | PNG 与 BMP 的 LLM 回答 | 完全相同 | | |
| | 流式编码 vs 整幅 `encode()` | batch ≥8 时逐位一致 | | |
| | 32768×30720 单帧 (opd) | 2.9 s,5365 crop/s,下行 1.11 MB,910.2×,峰值显存 20.16 GB | | |
| | 同一张 1024×768 上两个后端 | opd 864 B / 910.2× / 20.80 dB;rstok 6912 B / 113.8× / 27.56 dB | | |
| | rstok vendor 正确性 | bf16 下码字与官方参考 **100.00%** 一致;用官方码字解码与官方重建差 0.36/255 | | |
| `batch=1` 是唯一的例外:约 0.2%–0.5% 的码字会翻转(见上文「关于 batch」), | |
| 平均重建影响 <0.4/255。判据看的是重建像素,不是码字。 | |