chore: publish rSkill OpenRAL/rskill-robometer_4b-any-general-nf4 v0.1.0
Browse files- _vendor/build_experiment.py +340 -0
- _vendor/keydiff.py +60 -0
- _vendor/probe.py +116 -0
- _vendor/quant_probe.py +140 -0
- _vendor/reload_experiment.py +275 -0
_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())
|