AdrianLlopart commited on
Commit
5e3aa94
·
verified ·
1 Parent(s): 68b2f79

chore: publish rSkill OpenRAL/rskill-robometer_4b-any-general-nf4 v0.1.0

Browse files
_vendor/build_experiment.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Experiment: can we produce a PRE-quantized Robometer checkpoint that reloads
2
+ directly as 4-bit (no bf16 base+ckpt read, no requantize)? (Robometer NF4 load fix)
3
+
4
+ Measures the current load path's cost, then tries save_pretrained + reload.
5
+ Run: /tmp/robometer-env/bin/python rskills/robometer-4b/_vendor/build_experiment.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import pathlib
12
+ import resource
13
+ import time
14
+
15
+ # Deterministic cuBLAS so the reward ramp is byte-stable across process launches:
16
+ # without it, cuBLAS heuristic algo selection depends on process warmup history
17
+ # (a warmed process and a cold one differ ~0.006). Must precede CUDA init.
18
+ os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
19
+
20
+ import torch
21
+
22
+ torch.backends.cudnn.allow_tf32 = False
23
+ torch.use_deterministic_algorithms(True, warn_only=True)
24
+ # Force the math SDP kernel: flash/mem-efficient selection is process-state
25
+ # dependent (a warmed vs cold process picks different kernels -> ~0.006 drift).
26
+ torch.backends.cuda.enable_flash_sdp(False)
27
+ torch.backends.cuda.enable_mem_efficient_sdp(False)
28
+ torch.backends.cuda.enable_math_sdp(True)
29
+
30
+ OUT = pathlib.Path("/tmp/robometer-nf4-ckpt")
31
+ MIN_PARAMS = 4_000_000
32
+
33
+
34
+ def _rss_gb() -> float:
35
+ return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1e6 # KB->GB on linux
36
+
37
+
38
+ def _quantize_nf4_in_place(root: torch.nn.Module, compute_dtype: torch.dtype) -> int:
39
+ import bitsandbytes as bnb
40
+
41
+ n = 0
42
+
43
+ def _replace(m: torch.nn.Module) -> None:
44
+ nonlocal n
45
+ for name, child in list(m.named_children()):
46
+ if isinstance(child, torch.nn.Linear) and child.weight.numel() >= MIN_PARAMS:
47
+ new = bnb.nn.Linear4bit(
48
+ child.in_features,
49
+ child.out_features,
50
+ bias=child.bias is not None,
51
+ compute_dtype=compute_dtype,
52
+ quant_type="nf4",
53
+ )
54
+ new.weight = bnb.nn.Params4bit(
55
+ child.weight.data.clone(), requires_grad=False, quant_type="nf4"
56
+ )
57
+ if child.bias is not None:
58
+ new.bias = torch.nn.Parameter(
59
+ child.bias.data.clone().to(compute_dtype), requires_grad=False
60
+ )
61
+ setattr(m, name, new)
62
+ n += 1
63
+ else:
64
+ _replace(child)
65
+
66
+ _replace(root)
67
+ return n
68
+
69
+
70
+ def main() -> int:
71
+ from dataclasses import fields
72
+
73
+ import yaml
74
+ from huggingface_hub import hf_hub_download
75
+ from robometer.configs.experiment_configs import ExperimentConfig
76
+ from robometer.utils.save import resolve_checkpoint_path
77
+ from robometer.utils.setup_utils import setup_model_and_processor
78
+
79
+ t0 = time.monotonic()
80
+ print("[exp] load bf16 on CPU via VANILLA path (use_unsloth=False) ...", flush=True)
81
+ # Replicate load_model_from_hf's config assembly but force use_unsloth=False so
82
+ # the model is built with vanilla Qwen3VLModel naming (matches the meta reload).
83
+ resolved = resolve_checkpoint_path("robometer/Robometer-4B")
84
+ cfg_yaml = hf_hub_download("robometer/Robometer-4B", "config.yaml")
85
+ raw = yaml.safe_load(open(cfg_yaml))
86
+ valid = {f.name for f in fields(ExperimentConfig)}
87
+ exp_config = ExperimentConfig(**{k: v for k, v in raw.items() if k in valid})
88
+ exp_config.model.use_unsloth = False
89
+ tokenizer, processor, model = setup_model_and_processor(
90
+ exp_config.model, str(resolved), peft_config=None
91
+ )
92
+ model = model.to("cpu").eval()
93
+ t_load = time.monotonic() - t0
94
+ print(f"[exp] LOAD took {t_load:.1f}s; peak RSS {_rss_gb():.1f} GB", flush=True)
95
+
96
+ t1 = time.monotonic()
97
+ n = _quantize_nf4_in_place(model, compute_dtype=torch.bfloat16)
98
+ model.to("cuda")
99
+ torch.cuda.synchronize()
100
+ t_quant = time.monotonic() - t1
101
+ print(
102
+ f"[exp] QUANTIZE+to(cuda) took {t_quant:.1f}s; {n} modules; "
103
+ f"{torch.cuda.memory_allocated() / 1e9:.2f} GB VRAM",
104
+ flush=True,
105
+ )
106
+
107
+ # Save the packed state_dict explicitly (keys match a vanilla meta reload).
108
+ from safetensors.torch import save_file
109
+
110
+ OUT.mkdir(parents=True, exist_ok=True)
111
+ sd = {
112
+ k: (v.detach().contiguous() if hasattr(v, "detach") else v)
113
+ for k, v in model.state_dict().items()
114
+ }
115
+ # Fold in the NON-persistent buffers (rotary inv_freq etc.) that state_dict()
116
+ # omits, so the meta reload loads them bit-identically instead of recomputing
117
+ # them (recompute drifts ~0.004 on the progress series). assign=True on reload
118
+ # restores them exactly.
119
+ n_extra = 0
120
+ for name, buf in model.named_buffers():
121
+ if name not in sd and buf is not None:
122
+ sd[name] = buf.detach().contiguous()
123
+ n_extra += 1
124
+ print(f"[exp] folded {n_extra} non-persistent buffers into the checkpoint")
125
+ save_file(sd, str(OUT / "model.safetensors"))
126
+ # Save the SELF-CONTAINED processor/tokenizer/config (resized vocab + added
127
+ # progress token) so the meta reload never touches the base model.
128
+ processor.save_pretrained(str(OUT))
129
+ tokenizer.save_pretrained(str(OUT))
130
+ model.config.save_pretrained(str(OUT))
131
+ size_gb = (OUT / "model.safetensors").stat().st_size / 1e9
132
+ vis_keys = [k for k in sd if "visual.blocks.0.mlp.linear_fc1" in k]
133
+ print(f"[exp] SAVE OK: {size_gb:.2f} GB, {len(sd)} tensors")
134
+ print(f"[exp] vision mlp keys present (sample): {vis_keys}")
135
+
136
+ # Reference forward on the SAME 10 frames the reload test uses → series_A.
137
+ series = _forward_series(model, tokenizer, processor, exp_config)
138
+ print(
139
+ f"[exp] REFERENCE progress series (bf16+quantize, LIVE processor): "
140
+ f"{[round(float(x), 4) for x in series]}",
141
+ flush=True,
142
+ )
143
+
144
+ # Same weights, but recompute with the processor/tokenizer RELOADED from the
145
+ # checkpoint dir (what the meta reload uses). Isolates any processor
146
+ # save_pretrained round-trip drift from any weight/construction drift.
147
+ from transformers import AutoProcessor, AutoTokenizer
148
+
149
+ ck_proc = AutoProcessor.from_pretrained(str(OUT))
150
+ ck_tok = AutoTokenizer.from_pretrained(str(OUT))
151
+ series_ck = _forward_series(model, ck_tok, ck_proc, exp_config)
152
+ print(
153
+ f"[exp] REFERENCE progress series (bf16+quantize, CKPT processor): "
154
+ f"{[round(float(x), 4) for x in series_ck]}",
155
+ flush=True,
156
+ )
157
+
158
+ # --- In-process round-trip diagnostic: does from_prequantized reconstruct the
159
+ # SAME 4-bit weights as the in-place quantization the build forward used? ---
160
+ import bitsandbytes as bnb
161
+ from safetensors.torch import load_file as _load_file
162
+
163
+ probe = "model.language_model.layers.0.mlp.gate_proj"
164
+ live_mod = dict(model.named_modules())[probe]
165
+ saved = _load_file(str(OUT / "model.safetensors"), device="cuda")
166
+ stats = {
167
+ s.lstrip("."): saved[f"{probe}.weight{s}"]
168
+ for s in (
169
+ ".absmax",
170
+ ".quant_map",
171
+ ".nested_absmax",
172
+ ".nested_quant_map",
173
+ ".quant_state.bitsandbytes__nf4",
174
+ )
175
+ if f"{probe}.weight{s}" in saved
176
+ }
177
+ reloaded_w = bnb.nn.Params4bit.from_prequantized(
178
+ data=saved[f"{probe}.weight"], quantized_stats=stats, requires_grad=False, device="cuda"
179
+ )
180
+ dq_live = bnb.functional.dequantize_4bit(
181
+ live_mod.weight.data, live_mod.weight.quant_state
182
+ ).float()
183
+ dq_reload = bnb.functional.dequantize_4bit(reloaded_w.data, reloaded_w.quant_state).float()
184
+ max_abs = (dq_live - dq_reload).abs().max().item()
185
+ print(
186
+ f"[exp] DEQUANT max|live - from_prequantized| for {probe}: {max_abs:.3e} "
187
+ f"(0.0 => bit-identical 4-bit round-trip)",
188
+ flush=True,
189
+ )
190
+
191
+ # Report the global numerics flags this (build) process is running under, so we
192
+ # can compare them against the meta-reload process.
193
+ print(
194
+ f"[exp] tf32 matmul={torch.backends.cuda.matmul.allow_tf32} "
195
+ f"cudnn.tf32={torch.backends.cudnn.allow_tf32} "
196
+ f"fp32_precision(matmul)={torch.get_float32_matmul_precision()}",
197
+ flush=True,
198
+ )
199
+
200
+ # --- DEFINITIVE same-process A/B: construct the model the meta-reload way from
201
+ # the file we just saved, run the SAME forward, compare to the build forward.
202
+ # Identical process => identical env => isolates construction-path drift. ---
203
+ del live_mod, dq_live, dq_reload, reloaded_w, saved
204
+ torch.cuda.empty_cache()
205
+ series_meta = _meta_reload_series(exp_config, base_id="Qwen/Qwen3-VL-4B-Instruct")
206
+ print(
207
+ f"[exp] META-RELOAD progress series (same process): "
208
+ f"{[round(float(x), 4) for x in series_meta]}",
209
+ flush=True,
210
+ )
211
+ import numpy as np
212
+
213
+ dmax = float(np.abs(np.asarray(series) - np.asarray(series_meta)).max())
214
+ print(f"[exp] max|build - meta_reload| (same process) = {dmax:.3e}", flush=True)
215
+ return 0
216
+
217
+
218
+ def _meta_reload_series(exp_config, base_id):
219
+ """Build via meta + install_prequantized from OUT, return the progress series."""
220
+ import bitsandbytes as bnb
221
+ from robometer.models.rbm import RBM
222
+ from safetensors.torch import load_file
223
+ from transformers import AutoConfig, AutoProcessor, AutoTokenizer
224
+
225
+ config = AutoConfig.from_pretrained(str(OUT))
226
+ processor = AutoProcessor.from_pretrained(str(OUT))
227
+ tokenizer = AutoTokenizer.from_pretrained(str(OUT))
228
+ for c in (config, getattr(config, "text_config", None), getattr(config, "vision_config", None)):
229
+ if c is not None:
230
+ c._attn_implementation = "sdpa"
231
+ with torch.device("meta"):
232
+ model = RBM(
233
+ config,
234
+ processor,
235
+ tokenizer,
236
+ base_model=None,
237
+ base_model_id=base_id,
238
+ model_config=exp_config.model,
239
+ )
240
+
241
+ # Linear4bit shells for numel>=MIN_PARAMS (same rule as the build quantizer).
242
+ def _shells(m):
243
+ for name, child in list(m.named_children()):
244
+ if isinstance(child, torch.nn.Linear) and child.weight.numel() >= MIN_PARAMS:
245
+ setattr(
246
+ m,
247
+ name,
248
+ bnb.nn.Linear4bit(
249
+ child.in_features,
250
+ child.out_features,
251
+ bias=child.bias is not None,
252
+ compute_dtype=torch.bfloat16,
253
+ quant_type="nf4",
254
+ ),
255
+ )
256
+ else:
257
+ _shells(child)
258
+
259
+ _shells(model)
260
+
261
+ state = load_file(str(OUT / "model.safetensors"), device="cuda")
262
+ sufs = (
263
+ ".absmax",
264
+ ".quant_map",
265
+ ".nested_absmax",
266
+ ".nested_quant_map",
267
+ ".quant_state.bitsandbytes__nf4",
268
+ ".quant_state.bitsandbytes__fp4",
269
+ )
270
+ consumed: set[str] = set()
271
+ for prefix, module in model.named_modules():
272
+ if not isinstance(module, bnb.nn.Linear4bit):
273
+ continue
274
+ wkey = f"{prefix}.weight"
275
+ if wkey not in state:
276
+ continue
277
+ stats = {s.lstrip("."): state[f"{wkey}{s}"] for s in sufs if f"{wkey}{s}" in state}
278
+ for s in sufs:
279
+ consumed.add(f"{wkey}{s}")
280
+ consumed.add(wkey)
281
+ module.weight = bnb.nn.Params4bit.from_prequantized(
282
+ data=state[wkey], quantized_stats=stats, requires_grad=False, device="cuda"
283
+ )
284
+ bkey = f"{prefix}.bias"
285
+ if module.bias is not None and bkey in state:
286
+ module.bias = torch.nn.Parameter(state[bkey].to("cuda"), requires_grad=False)
287
+ consumed.add(bkey)
288
+ leftover = {k: v for k, v in state.items() if k not in consumed}
289
+ model.load_state_dict(leftover, strict=False, assign=True)
290
+ for bname, buf in list(model.named_buffers()):
291
+ if not buf.is_meta:
292
+ continue
293
+ parent = model.get_submodule(bname.rsplit(".", 1)[0]) if "." in bname else model
294
+ if bname in state:
295
+ parent.register_buffer(
296
+ bname.rsplit(".", 1)[-1], state[bname].to("cuda"), persistent=False
297
+ )
298
+ for _nm, mod in model.named_modules():
299
+ ifb = getattr(mod, "inv_freq", None)
300
+ if ifb is not None and hasattr(mod, "original_inv_freq"):
301
+ mod.original_inv_freq = ifb
302
+ return _forward_series(model, tokenizer, processor, exp_config)
303
+
304
+
305
+ def _forward_series(model, tokenizer, processor, exp_config):
306
+ """Run the discrete-mode progress forward on a fixed 10-frame clip."""
307
+ import decord
308
+ import numpy as np
309
+ from robometer.data.dataset_types import ProgressSample, Trajectory
310
+ from robometer.evals.eval_server import compute_batch_outputs
311
+ from robometer.utils.setup_utils import setup_batch_collator
312
+
313
+ model.eval()
314
+ vr = decord.VideoReader("/tmp/robometer_example.mp4")
315
+ step = max(1, int(round(vr.get_avg_fps() / 3.0)))
316
+ idx = list(range(0, len(vr), step))[:10]
317
+ frames = vr.get_batch(idx).asnumpy().astype(np.uint8)
318
+ collator = setup_batch_collator(processor, tokenizer, exp_config, is_eval=True)
319
+ traj = Trajectory(
320
+ frames=frames,
321
+ frames_shape=tuple(frames.shape),
322
+ task="Pick up the object and place it in the container",
323
+ id="0",
324
+ metadata={"subsequence_length": int(frames.shape[0])},
325
+ video_embeddings=None,
326
+ )
327
+ batch = collator([ProgressSample(trajectory=traj, sample_type="progress")])
328
+ inp = batch["progress_inputs"]
329
+ for k, v in inp.items():
330
+ if hasattr(v, "to"):
331
+ inp[k] = v.to("cuda")
332
+ with torch.no_grad():
333
+ res = compute_batch_outputs(
334
+ model, tokenizer, inp, sample_type="progress", is_discrete_mode=True, num_bins=100
335
+ )
336
+ return np.asarray(res["progress_pred"][0], dtype=np.float32)
337
+
338
+
339
+ if __name__ == "__main__":
340
+ raise SystemExit(main())
_vendor/keydiff.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Diagnose meta-skeleton ↔ checkpoint key divergence (Robometer NF4 load fix)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+ from dataclasses import fields
7
+
8
+ import torch
9
+ import yaml
10
+
11
+ CKPT = pathlib.Path("/tmp/robometer-nf4-ckpt/model.safetensors")
12
+
13
+
14
+ def main() -> int:
15
+ from huggingface_hub import hf_hub_download
16
+ from robometer.configs.experiment_configs import ExperimentConfig
17
+ from robometer.models.rbm import RBM
18
+ from safetensors import safe_open
19
+ from transformers import AutoConfig
20
+
21
+ base_id = "Qwen/Qwen3-VL-4B-Instruct"
22
+ raw = yaml.safe_load(open(hf_hub_download("robometer/Robometer-4B", "config.yaml")))
23
+ valid = {f.name for f in fields(ExperimentConfig)}
24
+ exp_config = ExperimentConfig(**{k: v for k, v in raw.items() if k in valid})
25
+
26
+ for cfg_src in ("robometer/Robometer-4B", base_id):
27
+ config = AutoConfig.from_pretrained(cfg_src)
28
+ with torch.device("meta"):
29
+ model = RBM(
30
+ config,
31
+ None,
32
+ None,
33
+ base_model=None,
34
+ base_model_id=base_id,
35
+ model_config=exp_config.model,
36
+ )
37
+ mkeys = set(model.state_dict().keys())
38
+ vis_fc1 = [k for k in mkeys if "visual.blocks.0.mlp" in k]
39
+ vt = type(dict(model.named_modules()).get("model.visual.blocks.0.mlp.linear_fc1"))
40
+ lin = dict(model.named_modules()).get("model.visual.blocks.0.mlp.linear_fc1")
41
+ numel = lin.weight.numel() if lin is not None else None
42
+ print(f"\n=== config from {cfg_src} ===")
43
+ print(f" model.visual.blocks.0.mlp keys: {vis_fc1}")
44
+ print(f" linear_fc1 type={vt} numel={numel}")
45
+
46
+ with safe_open(str(CKPT), framework="pt") as f:
47
+ skeys = set(f.keys())
48
+ print("\n=== checkpoint file ===")
49
+ print(f" total keys: {len(skeys)}")
50
+ print(f" visual.blocks.0.mlp keys: {sorted(k for k in skeys if 'visual.blocks.0.mlp' in k)}")
51
+ # model keys (last config = base) vs file
52
+ only_model = sorted(mkeys - skeys)[:8]
53
+ only_file = sorted(skeys - mkeys)[:8]
54
+ print(f"\n in model not file (sample): {only_model}")
55
+ print(f" in file not model (sample): {only_file}")
56
+ return 0
57
+
58
+
59
+ if __name__ == "__main__":
60
+ raise SystemExit(main())
_vendor/probe.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 0 load+forward probe for robometer/Robometer-4B (reward rSkill gating spike).
2
+
3
+ Mirrors robometer's scripts/example_inference_local.py but builds a tiny synthetic
4
+ clip programmatically (no example_videos needed, since we pip-installed the package
5
+ rather than cloning). Goal: confirm the model loads with inference-only deps and
6
+ print the exact output field names / shapes / value ranges for progress + success.
7
+
8
+ Run inside the isolated env:
9
+ /tmp/robometer-env/bin/python rskills/robometer-4b/_vendor/probe.py
10
+ Optional: --weights <dir> to probe a quantized checkpoint, --device cuda.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+
17
+ import numpy as np
18
+
19
+
20
+ def main() -> int:
21
+ ap = argparse.ArgumentParser()
22
+ ap.add_argument("--model-path", default="robometer/Robometer-4B")
23
+ ap.add_argument("--device", default="cpu")
24
+ ap.add_argument("--frames", type=int, default=8)
25
+ ap.add_argument("--hw", type=int, default=224)
26
+ ap.add_argument("--task", default="pick up the cube and place it in the bowl")
27
+ ap.add_argument("--discrete", action="store_true")
28
+ ap.add_argument("--num-bins", type=int, default=100)
29
+ args = ap.parse_args()
30
+
31
+ from robometer.data.dataset_types import ProgressSample, Trajectory
32
+ from robometer.evals.eval_server import compute_batch_outputs
33
+ from robometer.utils.save import load_model_from_hf
34
+ from robometer.utils.setup_utils import setup_batch_collator
35
+
36
+ print(f"[probe] loading {args.model_path} on {args.device} ...", flush=True)
37
+ exp_config, tokenizer, processor, reward_model = load_model_from_hf(
38
+ model_path=args.model_path,
39
+ device=args.device,
40
+ )
41
+ print(f"[probe] loaded model class = {type(reward_model).__name__}", flush=True)
42
+ print(f"[probe] exp_config type = {type(exp_config).__name__}", flush=True)
43
+
44
+ batch_collator = setup_batch_collator(processor, tokenizer, exp_config, is_eval=True)
45
+
46
+ T, H, W, C = args.frames, args.hw, args.hw, 3
47
+ video_frames = np.random.randint(0, 255, (T, H, W, C), dtype=np.uint8)
48
+ traj = Trajectory(
49
+ frames=video_frames,
50
+ frames_shape=tuple(video_frames.shape),
51
+ task=args.task,
52
+ id="0",
53
+ metadata={"subsequence_length": T},
54
+ video_embeddings=None,
55
+ )
56
+ progress_sample = ProgressSample(trajectory=traj, sample_type="progress")
57
+ batch = batch_collator([progress_sample])
58
+ # The collator returns a wrapper dict; the model inputs live under "progress_inputs".
59
+ progress_inputs = batch["progress_inputs"]
60
+ for key, value in progress_inputs.items():
61
+ if hasattr(value, "to"):
62
+ progress_inputs[key] = value.to(args.device)
63
+ reward_model.eval()
64
+
65
+ print(f"[probe] progress_inputs keys = {sorted(progress_inputs.keys())}", flush=True)
66
+ print("[probe] running compute_batch_outputs ...", flush=True)
67
+ results = compute_batch_outputs(
68
+ reward_model,
69
+ tokenizer,
70
+ progress_inputs,
71
+ sample_type="progress",
72
+ is_discrete_mode=args.discrete,
73
+ num_bins=args.num_bins,
74
+ )
75
+
76
+ print("\n=== RESULT KEYS ===")
77
+ print(sorted(results.keys()))
78
+
79
+ # success lives nested under outputs_success
80
+ outputs_success = results.get("outputs_success")
81
+ if isinstance(outputs_success, dict):
82
+ print(f"[probe] outputs_success keys = {sorted(outputs_success.keys())}")
83
+ for k, v in outputs_success.items():
84
+ try:
85
+ arr = np.asarray(v[0] if (isinstance(v, list) and v) else v, dtype=np.float32)
86
+ print(
87
+ f" outputs_success[{k}]: shape={arr.shape} "
88
+ f"min={arr.min():.4f} max={arr.max():.4f}"
89
+ if arr.size
90
+ else f" outputs_success[{k}]: empty"
91
+ )
92
+ except Exception as exc:
93
+ print(f" outputs_success[{k}]: <{type(v).__name__}> ({exc})")
94
+
95
+ def describe(name: str) -> None:
96
+ val = results.get(name)
97
+ if val is None:
98
+ print(f" {name}: <absent>")
99
+ return
100
+ arr = np.asarray(val[0] if (isinstance(val, list) and val) else val, dtype=np.float32)
101
+ if arr.size:
102
+ print(
103
+ f" {name}: shape={arr.shape} min={arr.min():.4f} max={arr.max():.4f} "
104
+ f"first5={np.round(arr.flatten()[:5], 4).tolist()}"
105
+ )
106
+ else:
107
+ print(f" {name}: empty")
108
+
109
+ print("\n=== PROGRESS / SUCCESS ===")
110
+ for key in ("progress_pred", "success_probs", "success_pred", "preference", "pref_logits"):
111
+ describe(key)
112
+ return 0
113
+
114
+
115
+ if __name__ == "__main__":
116
+ raise SystemExit(main())
_vendor/quant_probe.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Phase 2 NF4 quantization probe for Robometer-4B.
2
+
3
+ Replicates the repo's NF4 rewrite rule from openral_sim._quantization
4
+ (quantize_nf4_in_place: nn.Linear with weight.numel() >= 4M -> bnb.nn.Linear4bit,
5
+ quant_type="nf4", compute_dtype=bf16; pack happens on .to(cuda)). The rule is
6
+ inlined here because the isolated robometer venv cannot import openral_sim.
7
+
8
+ Loads RBM bf16 on CPU, quantizes, moves to CUDA, runs one forward, and reports
9
+ peak VRAM — the empirical answer to "how easy to quantize" + "does it leave 8 GB
10
+ headroom for a parallel VLA?".
11
+
12
+ /tmp/robometer-env/bin/python rskills/robometer-4b/_vendor/quant_probe.py
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import numpy as np
18
+ import torch
19
+
20
+ MIN_PARAMS = 4_000_000 # DEFAULT_MIN_PARAMS_TO_QUANTIZE (openral_sim._quantization)
21
+
22
+
23
+ def quantize_nf4_in_place(root: torch.nn.Module, compute_dtype: torch.dtype) -> int:
24
+ import bitsandbytes as bnb
25
+
26
+ n = 0
27
+
28
+ def _replace(module: torch.nn.Module, prefix: str = "") -> None:
29
+ nonlocal n
30
+ for name, child in list(module.named_children()):
31
+ if isinstance(child, torch.nn.Linear) and child.weight.numel() >= MIN_PARAMS:
32
+ new = bnb.nn.Linear4bit(
33
+ child.in_features,
34
+ child.out_features,
35
+ bias=child.bias is not None,
36
+ compute_dtype=compute_dtype,
37
+ quant_type="nf4",
38
+ )
39
+ new.weight = bnb.nn.Params4bit(
40
+ child.weight.data.clone(),
41
+ requires_grad=False,
42
+ quant_type="nf4",
43
+ )
44
+ if child.bias is not None:
45
+ new.bias = torch.nn.Parameter(
46
+ child.bias.data.clone().to(compute_dtype),
47
+ requires_grad=False,
48
+ )
49
+ setattr(module, name, new)
50
+ n += 1
51
+ else:
52
+ _replace(child, f"{prefix}.{name}" if prefix else name)
53
+
54
+ _replace(root)
55
+ return n
56
+
57
+
58
+ def main() -> int:
59
+ from robometer.data.dataset_types import ProgressSample, Trajectory
60
+ from robometer.evals.eval_server import compute_batch_outputs
61
+ from robometer.utils.save import load_model_from_hf
62
+ from robometer.utils.setup_utils import setup_batch_collator
63
+
64
+ assert torch.cuda.is_available(), "need CUDA for the VRAM measurement"
65
+
66
+ print("[quant] loading bf16 on CPU ...", flush=True)
67
+ exp_config, tokenizer, processor, reward_model = load_model_from_hf(
68
+ model_path="robometer/Robometer-4B",
69
+ device="cpu",
70
+ )
71
+ reward_model.eval()
72
+
73
+ print("[quant] rewriting large Linears -> NF4 ...", flush=True)
74
+ n = quantize_nf4_in_place(reward_model, compute_dtype=torch.bfloat16)
75
+ print(f"[quant] rewrote {n} Linear modules to NF4", flush=True)
76
+
77
+ torch.cuda.reset_peak_memory_stats()
78
+ print("[quant] moving to CUDA (packs nf4) ...", flush=True)
79
+ reward_model.to("cuda")
80
+ torch.cuda.synchronize()
81
+ resident = torch.cuda.memory_allocated() / 1e9
82
+ print(f"[quant] NF4 weights resident on CUDA: {resident:.2f} GB", flush=True)
83
+
84
+ # one forward to confirm correctness post-quant
85
+ batch_collator = setup_batch_collator(processor, tokenizer, exp_config, is_eval=True)
86
+ T = 8
87
+ frames = np.random.randint(0, 255, (T, 224, 224, 3), dtype=np.uint8)
88
+ traj = Trajectory(
89
+ frames=frames,
90
+ frames_shape=tuple(frames.shape),
91
+ task="pick up the cube",
92
+ id="0",
93
+ metadata={"subsequence_length": T},
94
+ video_embeddings=None,
95
+ )
96
+ batch = batch_collator([ProgressSample(trajectory=traj, sample_type="progress")])
97
+ progress_inputs = batch["progress_inputs"]
98
+ for k, v in progress_inputs.items():
99
+ if hasattr(v, "to"):
100
+ progress_inputs[k] = v.to("cuda")
101
+
102
+ print("[quant] running forward (discrete mode) ...", flush=True)
103
+ with torch.no_grad():
104
+ results = compute_batch_outputs(
105
+ reward_model,
106
+ tokenizer,
107
+ progress_inputs,
108
+ sample_type="progress",
109
+ is_discrete_mode=True,
110
+ num_bins=100,
111
+ )
112
+ torch.cuda.synchronize()
113
+ peak = torch.cuda.max_memory_allocated() / 1e9
114
+
115
+ prog = np.asarray(
116
+ results["progress_pred"][0]
117
+ if isinstance(results["progress_pred"], list)
118
+ else results["progress_pred"],
119
+ dtype=np.float32,
120
+ )
121
+ succ = results.get("outputs_success", {}).get("success_probs")
122
+ succ = (
123
+ np.asarray(succ[0] if isinstance(succ, list) and succ else succ, dtype=np.float32)
124
+ if succ is not None
125
+ else np.array([])
126
+ )
127
+
128
+ print("\n=== NF4 RESULT ===")
129
+ print(f" modules quantized: {n}")
130
+ print(f" NF4 resident VRAM: {resident:.2f} GB")
131
+ print(f" peak VRAM (incl. 8-frame forward activations): {peak:.2f} GB")
132
+ print(f" progress_pred: shape={prog.shape} range=[{prog.min():.4f},{prog.max():.4f}]")
133
+ if succ.size:
134
+ print(f" success_probs: shape={succ.shape} range=[{succ.min():.4f},{succ.max():.4f}]")
135
+ print(f" GPU total 8.0 GB -> headroom after peak: {8.0 - peak:.2f} GB")
136
+ return 0
137
+
138
+
139
+ if __name__ == "__main__":
140
+ raise SystemExit(main())
_vendor/reload_experiment.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Experiment: reload the pre-quantized 3.32 GB checkpoint WITHOUT the bf16 base
2
+ load — build the RBM skeleton from config (random init), install Linear4bit, then
3
+ load_state_dict the packed NF4 weights. Verify forward reproduces the ramp.
4
+
5
+ Prereq: run build_experiment.py first (writes /tmp/robometer-nf4-ckpt/model.safetensors).
6
+ Run: /tmp/robometer-env/bin/python rskills/robometer-4b/_vendor/reload_experiment.py
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import pathlib
13
+ import resource
14
+ import time
15
+ from dataclasses import fields
16
+
17
+ import numpy as np
18
+ import yaml
19
+
20
+ # Match build_experiment: deterministic cuBLAS for byte-stable cross-process output.
21
+ os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
22
+
23
+ import torch
24
+
25
+ torch.backends.cudnn.allow_tf32 = False
26
+ torch.use_deterministic_algorithms(True, warn_only=True)
27
+ torch.backends.cuda.enable_flash_sdp(False)
28
+ torch.backends.cuda.enable_mem_efficient_sdp(False)
29
+ torch.backends.cuda.enable_math_sdp(True)
30
+
31
+ CKPT = pathlib.Path("/tmp/robometer-nf4-ckpt")
32
+ MIN_PARAMS = 4_000_000
33
+
34
+
35
+ def _rss_gb() -> float:
36
+ return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1e6
37
+
38
+
39
+ _BNB_META_SUFFIXES = (
40
+ ".absmax",
41
+ ".quant_map",
42
+ ".nested_absmax",
43
+ ".nested_quant_map",
44
+ ".quant_state.bitsandbytes__nf4",
45
+ ".quant_state.bitsandbytes__fp4",
46
+ )
47
+
48
+
49
+ def _install_prequantized(policy, state, device):
50
+ """Inlined openral_sim._quantization.install_prequantized_linears."""
51
+ import bitsandbytes as bnb
52
+
53
+ consumed: set[str] = set()
54
+ count = 0
55
+ for prefix, module in policy.named_modules():
56
+ if not isinstance(module, bnb.nn.Linear4bit):
57
+ continue
58
+ wkey = f"{prefix}.weight"
59
+ if wkey not in state:
60
+ continue
61
+ stats = {}
62
+ for suf in _BNB_META_SUFFIXES:
63
+ full = f"{wkey}{suf}"
64
+ if full in state:
65
+ stats[suf.lstrip(".")] = state[full]
66
+ consumed.add(full)
67
+ consumed.add(wkey)
68
+ module.weight = bnb.nn.Params4bit.from_prequantized(
69
+ data=state[wkey], quantized_stats=stats, requires_grad=False, device=device
70
+ )
71
+ bkey = f"{prefix}.bias"
72
+ if module.bias is not None and bkey in state:
73
+ module.bias = torch.nn.Parameter(state[bkey].to(device), requires_grad=False)
74
+ consumed.add(bkey)
75
+ count += 1
76
+ return count, consumed
77
+
78
+
79
+ def _quantize_structure(root, compute_dtype):
80
+ """Replace large Linears with Linear4bit (empty packed params) — structure only."""
81
+ import bitsandbytes as bnb
82
+
83
+ n = 0
84
+
85
+ def _replace(m):
86
+ nonlocal n
87
+ for name, child in list(m.named_children()):
88
+ if isinstance(child, torch.nn.Linear) and child.weight.numel() >= MIN_PARAMS:
89
+ new = bnb.nn.Linear4bit(
90
+ child.in_features,
91
+ child.out_features,
92
+ bias=child.bias is not None,
93
+ compute_dtype=compute_dtype,
94
+ quant_type="nf4",
95
+ )
96
+ setattr(m, name, new)
97
+ n += 1
98
+ else:
99
+ _replace(child)
100
+
101
+ _replace(root)
102
+ return n
103
+
104
+
105
+ def main() -> int:
106
+ from huggingface_hub import hf_hub_download
107
+ from robometer.configs.experiment_configs import ExperimentConfig
108
+ from robometer.models.rbm import RBM
109
+ from safetensors.torch import load_file
110
+ from transformers import AutoConfig, AutoProcessor, AutoTokenizer
111
+
112
+ base_id = "Qwen/Qwen3-VL-4B-Instruct"
113
+ # cheap pieces — no big model weights
114
+ cfg_yaml = hf_hub_download("robometer/Robometer-4B", "config.yaml")
115
+ raw = yaml.safe_load(open(cfg_yaml))
116
+ valid = {f.name for f in fields(ExperimentConfig)}
117
+ exp_config = ExperimentConfig(**{k: v for k, v in raw.items() if k in valid})
118
+
119
+ # Load config + processor + tokenizer from the SELF-CONTAINED checkpoint dir
120
+ # (resized vocab 151674 + robometer's added progress token), NOT the base.
121
+ config = AutoConfig.from_pretrained(str(CKPT))
122
+ processor = AutoProcessor.from_pretrained(str(CKPT))
123
+ tokenizer = AutoTokenizer.from_pretrained(str(CKPT))
124
+ # Direct construction (not from_pretrained) skips transformers' attn auto-select
125
+ # and defaults to "eager"; production's setup_model_and_processor uses "sdpa"
126
+ # (flash-attn absent). Force sdpa on every (sub)config so the meta path is
127
+ # numerically identical to the bf16+quantize reference.
128
+ for c in (config, getattr(config, "text_config", None), getattr(config, "vision_config", None)):
129
+ if c is not None:
130
+ c._attn_implementation = "sdpa"
131
+
132
+ print(
133
+ f"[reload] tf32 matmul={torch.backends.cuda.matmul.allow_tf32} "
134
+ f"cudnn.tf32={torch.backends.cudnn.allow_tf32} "
135
+ f"fp32_precision(matmul)={torch.get_float32_matmul_precision()}",
136
+ flush=True,
137
+ )
138
+
139
+ t0 = time.monotonic()
140
+ print("[reload] building RBM skeleton on META (instant, no weights) ...", flush=True)
141
+ with torch.device("meta"):
142
+ model = RBM(
143
+ config,
144
+ processor,
145
+ tokenizer,
146
+ base_model=None,
147
+ base_model_id=base_id,
148
+ model_config=exp_config.model,
149
+ )
150
+ n = _quantize_structure(model, compute_dtype=torch.bfloat16)
151
+ print(
152
+ f"[reload] meta skeleton + {n} Linear4bit shells in "
153
+ f"{time.monotonic() - t0:.1f}s; peak RSS {_rss_gb():.1f} GB",
154
+ flush=True,
155
+ )
156
+
157
+ t1 = time.monotonic()
158
+ state = load_file(str(CKPT / "model.safetensors"), device="cuda")
159
+ # 4-bit modules: rebuild packed weights directly on CUDA (no bf16 alloc).
160
+ n_q, consumed = _install_prequantized(model, state, device="cuda")
161
+ # everything else (embeddings, norms, heads): assign cuda tensors to meta params.
162
+ leftover = {k: v for k, v in state.items() if k not in consumed}
163
+ missing, unexpected = model.load_state_dict(leftover, strict=False, assign=True)
164
+ # load_state_dict SKIPS non-persistent buffers (rotary inv_freq) — it reports
165
+ # them as `unexpected` and never assigns them. So assign them by hand from the
166
+ # checkpoint, by dotted name, bit-identically (no recompute). Only truly-absent
167
+ # buffers fall back to recompute.
168
+ loaded_bufs, recomputed_meta = 0, 0
169
+ for bname, buf in list(model.named_buffers()):
170
+ if not buf.is_meta:
171
+ continue
172
+ parent = model.get_submodule(bname.rsplit(".", 1)[0]) if "." in bname else model
173
+ leaf = bname.rsplit(".", 1)[-1]
174
+ if bname in state: # persisted in the checkpoint -> exact restore
175
+ parent.register_buffer(leaf, state[bname].to("cuda"), persistent=False)
176
+ loaded_bufs += 1
177
+ elif hasattr(parent, "rope_init_fn") and hasattr(parent, "config"):
178
+ inv_freq, scaling = parent.rope_init_fn(parent.config, "cuda")
179
+ parent.register_buffer(leaf, inv_freq, persistent=False)
180
+ if hasattr(parent, "attention_scaling"):
181
+ parent.attention_scaling = scaling
182
+ recomputed_meta += 1
183
+ else: # Qwen3VLVisionRotaryEmbedding closed form
184
+ dim = 2 * buf.shape[0]
185
+ inv_freq = 1.0 / (
186
+ 10000.0 ** (torch.arange(0, dim, 2, dtype=torch.float, device="cuda") / dim)
187
+ )
188
+ parent.register_buffer(leaf, inv_freq, persistent=False)
189
+ recomputed_meta += 1
190
+ # Rebind the dangling non-buffer rope attributes off the (now-real) inv_freq.
191
+ for _nm, mod in model.named_modules():
192
+ ifb = getattr(mod, "inv_freq", None)
193
+ if ifb is None:
194
+ continue
195
+ if hasattr(mod, "original_inv_freq"):
196
+ mod.original_inv_freq = ifb
197
+ if (
198
+ hasattr(mod, "rope_init_fn")
199
+ and hasattr(mod, "config")
200
+ and getattr(mod, "attention_scaling", None) is None
201
+ ):
202
+ _, scaling = mod.rope_init_fn(mod.config, "cuda")
203
+ mod.attention_scaling = scaling
204
+ print(
205
+ f"[reload] rotary buffers loaded-from-ckpt={loaded_bufs} "
206
+ f"recomputed-from-meta(expect 0)={recomputed_meta}"
207
+ )
208
+
209
+ # any params/buffers still on meta (not in the checkpoint)?
210
+ still_meta = [n for n, p in model.named_parameters() if p.is_meta]
211
+ still_meta_buf = [n for n, b in model.named_buffers() if b.is_meta]
212
+ print(f"[reload] meta params left: {still_meta[:6]}")
213
+ print(f"[reload] meta buffers left after rotary fix: {still_meta_buf}")
214
+ # sanity: is a vision weight real (install handled it)?
215
+ vw = dict(model.named_parameters()).get("model.visual.blocks.0.mlp.linear_fc1.weight")
216
+ print(
217
+ f"[reload] vision fc1 weight is_meta={vw.is_meta if vw is not None else 'absent'} "
218
+ f"dtype={vw.dtype if vw is not None else '-'}"
219
+ )
220
+ torch.cuda.synchronize()
221
+ print(
222
+ f"[reload] install_prequantized({n_q}) + load_state_dict in {time.monotonic() - t1:.1f}s; "
223
+ f"{torch.cuda.memory_allocated() / 1e9:.2f} GB VRAM; "
224
+ f"missing={len(missing)} unexpected={len(unexpected)} "
225
+ f"meta_params_left={len(still_meta)} meta_bufs_left={len(still_meta_buf)}",
226
+ flush=True,
227
+ )
228
+ if missing:
229
+ print(f"[reload] sample missing keys: {missing[:5]}")
230
+ if unexpected:
231
+ print(f"[reload] sample unexpected keys: {unexpected[:5]}")
232
+
233
+ # forward on the real video → expect the ramp
234
+ model.eval()
235
+ import decord
236
+ from robometer.data.dataset_types import ProgressSample, Trajectory
237
+ from robometer.evals.eval_server import compute_batch_outputs
238
+ from robometer.utils.setup_utils import setup_batch_collator
239
+
240
+ vr = decord.VideoReader("/tmp/robometer_example.mp4")
241
+ step = max(1, int(round(vr.get_avg_fps() / 3.0)))
242
+ idx = list(range(0, len(vr), step))[:10]
243
+ frames = vr.get_batch(idx).asnumpy().astype(np.uint8)
244
+ collator = setup_batch_collator(processor, tokenizer, exp_config, is_eval=True)
245
+ traj = Trajectory(
246
+ frames=frames,
247
+ frames_shape=tuple(frames.shape),
248
+ task="Pick up the object and place it in the container",
249
+ id="0",
250
+ metadata={"subsequence_length": int(frames.shape[0])},
251
+ video_embeddings=None,
252
+ )
253
+ batch = collator([ProgressSample(trajectory=traj, sample_type="progress")])
254
+ inp = batch["progress_inputs"]
255
+ for k, v in inp.items():
256
+ if hasattr(v, "to"):
257
+ inp[k] = v.to("cuda")
258
+ with torch.no_grad():
259
+ res = compute_batch_outputs(
260
+ model, tokenizer, inp, sample_type="progress", is_discrete_mode=True, num_bins=100
261
+ )
262
+ prog = np.asarray(res["progress_pred"][0], dtype=np.float32)
263
+ print(
264
+ f"[reload] progress series (meta+prequantized path): {[round(float(x), 4) for x in prog]}"
265
+ )
266
+ print(
267
+ "[reload] COMPARE to the build_experiment REFERENCE series — they must "
268
+ "match element-wise to ~1e-3 for the prequantized load to be trusted.",
269
+ flush=True,
270
+ )
271
+ return 0
272
+
273
+
274
+ if __name__ == "__main__":
275
+ raise SystemExit(main())