AllenBi21 commited on
Commit
295ee70
·
verified ·
1 Parent(s): d17a053

Qistee/HTT integration: feature_policy.py + README (validated bit-identical)

Browse files
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)