Qistee/HTT integration: feature_policy.py + README (validated bit-identical)
Browse files- qistee_integration/README.md +47 -0
- qistee_integration/feature_policy.py +102 -0
qistee_integration/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# T3 / SITR inference in the Qistee/HTT robot pipeline
|
| 2 |
+
|
| 3 |
+
`2026-08-12` · validated end-to-end on the main workstation: FeaturePolicy
|
| 4 |
+
action chunks are BIT-IDENTICAL (max diff 0.0) to the tactile_act reference
|
| 5 |
+
inference on the same inputs; TorchScript features match stored training
|
| 6 |
+
features at cos ≥ 0.99997.
|
| 7 |
+
|
| 8 |
+
## Why sitr/t3 could not run in the existing pipeline
|
| 9 |
+
|
| 10 |
+
`htt_policy.py` is hard-wired to the in-graph HTT checkpoint:
|
| 11 |
+
1. `checkpoint_config()` hardcodes `tactile_dim=960` — T3/SITR are 3840;
|
| 12 |
+
2. `HTTPolicy` always builds `TactileImageBranch` and loads `strict=True` —
|
| 13 |
+
T3/SITR checkpoints have no `tactile_branch.*` weights;
|
| 14 |
+
3. its preprocessing (bg-subtract + 2-frame pairs) is the HTT convention —
|
| 15 |
+
T3/SITR features come from raw single frames with their own normalizations.
|
| 16 |
+
|
| 17 |
+
## Fix (3 files, no third-party code needed — only torch)
|
| 18 |
+
|
| 19 |
+
1. Copy `feature_policy.py` (this folder) into the Qistee/HTT repo root.
|
| 20 |
+
2. Download the TorchScript encoders from
|
| 21 |
+
hf.co/AllenBi21/tactile-act-screw-baselines → `exported/`:
|
| 22 |
+
`t3_feature_encoder.ts.pt`, `sitr_feature_encoder.ts.pt` (~345 MB each;
|
| 23 |
+
encoder-specific normalization baked into the graphs).
|
| 24 |
+
3. Checkpoints: `screw_t3_50k/act_best.pt`, `screw_sitr5_50k/act_best.pt`
|
| 25 |
+
(same HF repo).
|
| 26 |
+
|
| 27 |
+
Robot loop (mirror inference.py's sharpa mode, minus background capture —
|
| 28 |
+
T3/SITR need NO background, NO frame pairs):
|
| 29 |
+
|
| 30 |
+
```python
|
| 31 |
+
from feature_policy import load_feature_policy, TSFeatureEncoder
|
| 32 |
+
policy = load_feature_policy("checkpoint/t3_act_best.pt", device)
|
| 33 |
+
encoder = TSFeatureEncoder("exported/t3_feature_encoder.ts.pt", device)
|
| 34 |
+
...
|
| 35 |
+
feats = encoder(hand.raw_frame()) # (5,240,320) -> (1,3840)
|
| 36 |
+
chunk = policy.predict_action_chunk(qpos_t, feats)[0]
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Keep 30 Hz and the 30-step chunk queue exactly as in the HTT path. Finger
|
| 40 |
+
order 0..4 must match the camera order used in training (same as HTT's).
|
| 41 |
+
|
| 42 |
+
## Sanity check before robot use
|
| 43 |
+
|
| 44 |
+
Load an episode HDF5 from hf datasets AllenBi21/tactile-act-screw-episodes,
|
| 45 |
+
feed frame t's five raw images + qpos, and confirm the first action of the
|
| 46 |
+
chunk changes smoothly across consecutive t (and features vs stored /t3//sitr
|
| 47 |
+
groups have cosine > 0.999 — same test as tools/live_features.py --smoke).
|
qistee_integration/feature_policy.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ACT policies conditioned on EXTERNAL tactile features (T3 / SITR).
|
| 2 |
+
|
| 3 |
+
Counterpart of ``htt_policy.py`` for the ``screw_t3_50k`` / ``screw_sitr5_50k``
|
| 4 |
+
checkpoints (hf.co/AllenBi21/tactile-act-screw-baselines). Differences vs the
|
| 5 |
+
HTT policy:
|
| 6 |
+
|
| 7 |
+
* tactile_dim = 3840 (5 fingers x 768), read from the checkpoint config —
|
| 8 |
+
NOT hardcoded;
|
| 9 |
+
* there is NO in-graph tactile branch: features come from a TorchScript
|
| 10 |
+
encoder (``t3_feature_encoder.ts.pt`` / ``sitr_feature_encoder.ts.pt``,
|
| 11 |
+
same HF repo, ``exported/``) — no third-party code needed, only torch;
|
| 12 |
+
* NO background subtraction and NO frame pairs: both encoders consume raw
|
| 13 |
+
single frames (their authors' released conventions; normalization
|
| 14 |
+
constants are baked inside the TorchScript graphs).
|
| 15 |
+
|
| 16 |
+
Robot loop (mirrors inference.py's sharpa mode, minus background capture):
|
| 17 |
+
|
| 18 |
+
policy = load_feature_policy("checkpoint/act_best.pt", device)
|
| 19 |
+
encoder = TSFeatureEncoder("exported/t3_feature_encoder.ts.pt", device)
|
| 20 |
+
...
|
| 21 |
+
feats = encoder(hand.raw_frame()) # (5,240,320) uint8 -> (3840,)
|
| 22 |
+
chunk = policy.predict_action_chunk(qpos_t, feats)
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn.functional as F
|
| 29 |
+
from torch import Tensor, nn
|
| 30 |
+
|
| 31 |
+
from act_policy.config import ACTConfig
|
| 32 |
+
from act_policy.model import ACT
|
| 33 |
+
from act_policy.policy import _Normalizer
|
| 34 |
+
|
| 35 |
+
FINGER_COUNT = 5
|
| 36 |
+
FEATURE_DIM = 768
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class FeaturePolicy(nn.Module):
|
| 40 |
+
"""Inference-only ACT whose state-dict names match the feature checkpoints
|
| 41 |
+
(same layout as HTTPolicy minus ``tactile_branch``)."""
|
| 42 |
+
|
| 43 |
+
def __init__(self, config: ACTConfig) -> None:
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.config = config
|
| 46 |
+
self.model = ACT(config)
|
| 47 |
+
self.state_norm = _Normalizer(config.state_dim)
|
| 48 |
+
self.tactile_norm = _Normalizer(config.tactile_dim)
|
| 49 |
+
self.action_norm = _Normalizer(config.action_dim)
|
| 50 |
+
self.register_buffer("image_mean", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))
|
| 51 |
+
self.register_buffer("image_std", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))
|
| 52 |
+
|
| 53 |
+
@torch.inference_mode()
|
| 54 |
+
def predict_action_chunk(self, qpos: Tensor, tactile_features: Tensor) -> Tensor:
|
| 55 |
+
"""qpos (B,22); tactile_features (B,3840) from the TorchScript encoder."""
|
| 56 |
+
self.eval()
|
| 57 |
+
batch = {
|
| 58 |
+
"state": self.state_norm.normalize(qpos),
|
| 59 |
+
"tactile": self.tactile_norm.normalize(tactile_features),
|
| 60 |
+
}
|
| 61 |
+
actions, _ = self.model(batch)
|
| 62 |
+
return self.action_norm.unnormalize(actions)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def load_feature_policy(checkpoint: str, device: torch.device) -> FeaturePolicy:
|
| 66 |
+
payload = torch.load(checkpoint, map_location="cpu", weights_only=False)
|
| 67 |
+
cfg_dict = dict(payload["config"])
|
| 68 |
+
fields = {f for f in ACTConfig.__dataclass_fields__} # type: ignore[attr-defined]
|
| 69 |
+
cfg_dict = {k: v for k, v in cfg_dict.items() if k in fields}
|
| 70 |
+
cfg_dict["image_keys"] = []
|
| 71 |
+
config = ACTConfig(**cfg_dict)
|
| 72 |
+
if config.tactile_dim != FINGER_COUNT * FEATURE_DIM:
|
| 73 |
+
raise ValueError(
|
| 74 |
+
f"checkpoint tactile_dim={config.tactile_dim}, expected "
|
| 75 |
+
f"{FINGER_COUNT * FEATURE_DIM} — is this a T3/SITR checkpoint? "
|
| 76 |
+
"(HTT in-graph checkpoints belong to htt_policy.py)")
|
| 77 |
+
policy = FeaturePolicy(config)
|
| 78 |
+
policy.load_state_dict(payload["model"], strict=True)
|
| 79 |
+
return policy.to(device).eval()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class TSFeatureEncoder:
|
| 83 |
+
"""TorchScript T3/SITR feature encoder. Input: (5,H,W) uint8/float raw
|
| 84 |
+
grayscale fingertip frames (fingers 0..4). Output: (1,3840) float32.
|
| 85 |
+
Preprocessing here = /255 -> 3ch -> bilinear 224; the encoder-specific
|
| 86 |
+
normalization is baked into the TorchScript graph."""
|
| 87 |
+
|
| 88 |
+
def __init__(self, path: str, device: torch.device) -> None:
|
| 89 |
+
self.module = torch.jit.load(path, map_location=device).eval()
|
| 90 |
+
self.device = device
|
| 91 |
+
|
| 92 |
+
@torch.inference_mode()
|
| 93 |
+
def __call__(self, frames) -> Tensor:
|
| 94 |
+
x = torch.as_tensor(frames, device=self.device).float()
|
| 95 |
+
if x.ndim != 3 or x.shape[0] != FINGER_COUNT:
|
| 96 |
+
raise ValueError(f"expected (5,H,W) raw frames, got {tuple(x.shape)}")
|
| 97 |
+
if x.max() > 1.0:
|
| 98 |
+
x = x / 255.0
|
| 99 |
+
x = x.unsqueeze(1).expand(-1, 3, -1, -1)
|
| 100 |
+
x = F.interpolate(x, size=(224, 224), mode="bilinear", align_corners=False)
|
| 101 |
+
feats = self.module(x) # (5, 768), finger order kept
|
| 102 |
+
return feats.reshape(1, FINGER_COUNT * FEATURE_DIM)
|