AdrianLlopart's picture
chore: publish rSkill OpenRAL/rskill-robometer_4b-any-general-nf4 v0.1.0
5e3aa94 verified
Raw
History Blame Contribute Delete
11 kB
"""Experiment: reload the pre-quantized 3.32 GB checkpoint WITHOUT the bf16 base
load — build the RBM skeleton from config (random init), install Linear4bit, then
load_state_dict the packed NF4 weights. Verify forward reproduces the ramp.
Prereq: run build_experiment.py first (writes /tmp/robometer-nf4-ckpt/model.safetensors).
Run: /tmp/robometer-env/bin/python rskills/robometer-4b/_vendor/reload_experiment.py
"""
from __future__ import annotations
import os
import pathlib
import resource
import time
from dataclasses import fields
import numpy as np
import yaml
# Match build_experiment: deterministic cuBLAS for byte-stable cross-process output.
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
import torch
torch.backends.cudnn.allow_tf32 = False
torch.use_deterministic_algorithms(True, warn_only=True)
torch.backends.cuda.enable_flash_sdp(False)
torch.backends.cuda.enable_mem_efficient_sdp(False)
torch.backends.cuda.enable_math_sdp(True)
CKPT = pathlib.Path("/tmp/robometer-nf4-ckpt")
MIN_PARAMS = 4_000_000
def _rss_gb() -> float:
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1e6
_BNB_META_SUFFIXES = (
".absmax",
".quant_map",
".nested_absmax",
".nested_quant_map",
".quant_state.bitsandbytes__nf4",
".quant_state.bitsandbytes__fp4",
)
def _install_prequantized(policy, state, device):
"""Inlined openral_sim._quantization.install_prequantized_linears."""
import bitsandbytes as bnb
consumed: set[str] = set()
count = 0
for prefix, module in policy.named_modules():
if not isinstance(module, bnb.nn.Linear4bit):
continue
wkey = f"{prefix}.weight"
if wkey not in state:
continue
stats = {}
for suf in _BNB_META_SUFFIXES:
full = f"{wkey}{suf}"
if full in state:
stats[suf.lstrip(".")] = state[full]
consumed.add(full)
consumed.add(wkey)
module.weight = bnb.nn.Params4bit.from_prequantized(
data=state[wkey], quantized_stats=stats, requires_grad=False, device=device
)
bkey = f"{prefix}.bias"
if module.bias is not None and bkey in state:
module.bias = torch.nn.Parameter(state[bkey].to(device), requires_grad=False)
consumed.add(bkey)
count += 1
return count, consumed
def _quantize_structure(root, compute_dtype):
"""Replace large Linears with Linear4bit (empty packed params) — structure only."""
import bitsandbytes as bnb
n = 0
def _replace(m):
nonlocal n
for name, child in list(m.named_children()):
if isinstance(child, torch.nn.Linear) and child.weight.numel() >= MIN_PARAMS:
new = bnb.nn.Linear4bit(
child.in_features,
child.out_features,
bias=child.bias is not None,
compute_dtype=compute_dtype,
quant_type="nf4",
)
setattr(m, name, new)
n += 1
else:
_replace(child)
_replace(root)
return n
def main() -> int:
from huggingface_hub import hf_hub_download
from robometer.configs.experiment_configs import ExperimentConfig
from robometer.models.rbm import RBM
from safetensors.torch import load_file
from transformers import AutoConfig, AutoProcessor, AutoTokenizer
base_id = "Qwen/Qwen3-VL-4B-Instruct"
# cheap pieces — no big model weights
cfg_yaml = hf_hub_download("robometer/Robometer-4B", "config.yaml")
raw = yaml.safe_load(open(cfg_yaml))
valid = {f.name for f in fields(ExperimentConfig)}
exp_config = ExperimentConfig(**{k: v for k, v in raw.items() if k in valid})
# Load config + processor + tokenizer from the SELF-CONTAINED checkpoint dir
# (resized vocab 151674 + robometer's added progress token), NOT the base.
config = AutoConfig.from_pretrained(str(CKPT))
processor = AutoProcessor.from_pretrained(str(CKPT))
tokenizer = AutoTokenizer.from_pretrained(str(CKPT))
# Direct construction (not from_pretrained) skips transformers' attn auto-select
# and defaults to "eager"; production's setup_model_and_processor uses "sdpa"
# (flash-attn absent). Force sdpa on every (sub)config so the meta path is
# numerically identical to the bf16+quantize reference.
for c in (config, getattr(config, "text_config", None), getattr(config, "vision_config", None)):
if c is not None:
c._attn_implementation = "sdpa"
print(
f"[reload] tf32 matmul={torch.backends.cuda.matmul.allow_tf32} "
f"cudnn.tf32={torch.backends.cudnn.allow_tf32} "
f"fp32_precision(matmul)={torch.get_float32_matmul_precision()}",
flush=True,
)
t0 = time.monotonic()
print("[reload] building RBM skeleton on META (instant, no weights) ...", flush=True)
with torch.device("meta"):
model = RBM(
config,
processor,
tokenizer,
base_model=None,
base_model_id=base_id,
model_config=exp_config.model,
)
n = _quantize_structure(model, compute_dtype=torch.bfloat16)
print(
f"[reload] meta skeleton + {n} Linear4bit shells in "
f"{time.monotonic() - t0:.1f}s; peak RSS {_rss_gb():.1f} GB",
flush=True,
)
t1 = time.monotonic()
state = load_file(str(CKPT / "model.safetensors"), device="cuda")
# 4-bit modules: rebuild packed weights directly on CUDA (no bf16 alloc).
n_q, consumed = _install_prequantized(model, state, device="cuda")
# everything else (embeddings, norms, heads): assign cuda tensors to meta params.
leftover = {k: v for k, v in state.items() if k not in consumed}
missing, unexpected = model.load_state_dict(leftover, strict=False, assign=True)
# load_state_dict SKIPS non-persistent buffers (rotary inv_freq) — it reports
# them as `unexpected` and never assigns them. So assign them by hand from the
# checkpoint, by dotted name, bit-identically (no recompute). Only truly-absent
# buffers fall back to recompute.
loaded_bufs, recomputed_meta = 0, 0
for bname, buf in list(model.named_buffers()):
if not buf.is_meta:
continue
parent = model.get_submodule(bname.rsplit(".", 1)[0]) if "." in bname else model
leaf = bname.rsplit(".", 1)[-1]
if bname in state: # persisted in the checkpoint -> exact restore
parent.register_buffer(leaf, state[bname].to("cuda"), persistent=False)
loaded_bufs += 1
elif hasattr(parent, "rope_init_fn") and hasattr(parent, "config"):
inv_freq, scaling = parent.rope_init_fn(parent.config, "cuda")
parent.register_buffer(leaf, inv_freq, persistent=False)
if hasattr(parent, "attention_scaling"):
parent.attention_scaling = scaling
recomputed_meta += 1
else: # Qwen3VLVisionRotaryEmbedding closed form
dim = 2 * buf.shape[0]
inv_freq = 1.0 / (
10000.0 ** (torch.arange(0, dim, 2, dtype=torch.float, device="cuda") / dim)
)
parent.register_buffer(leaf, inv_freq, persistent=False)
recomputed_meta += 1
# Rebind the dangling non-buffer rope attributes off the (now-real) inv_freq.
for _nm, mod in model.named_modules():
ifb = getattr(mod, "inv_freq", None)
if ifb is None:
continue
if hasattr(mod, "original_inv_freq"):
mod.original_inv_freq = ifb
if (
hasattr(mod, "rope_init_fn")
and hasattr(mod, "config")
and getattr(mod, "attention_scaling", None) is None
):
_, scaling = mod.rope_init_fn(mod.config, "cuda")
mod.attention_scaling = scaling
print(
f"[reload] rotary buffers loaded-from-ckpt={loaded_bufs} "
f"recomputed-from-meta(expect 0)={recomputed_meta}"
)
# any params/buffers still on meta (not in the checkpoint)?
still_meta = [n for n, p in model.named_parameters() if p.is_meta]
still_meta_buf = [n for n, b in model.named_buffers() if b.is_meta]
print(f"[reload] meta params left: {still_meta[:6]}")
print(f"[reload] meta buffers left after rotary fix: {still_meta_buf}")
# sanity: is a vision weight real (install handled it)?
vw = dict(model.named_parameters()).get("model.visual.blocks.0.mlp.linear_fc1.weight")
print(
f"[reload] vision fc1 weight is_meta={vw.is_meta if vw is not None else 'absent'} "
f"dtype={vw.dtype if vw is not None else '-'}"
)
torch.cuda.synchronize()
print(
f"[reload] install_prequantized({n_q}) + load_state_dict in {time.monotonic() - t1:.1f}s; "
f"{torch.cuda.memory_allocated() / 1e9:.2f} GB VRAM; "
f"missing={len(missing)} unexpected={len(unexpected)} "
f"meta_params_left={len(still_meta)} meta_bufs_left={len(still_meta_buf)}",
flush=True,
)
if missing:
print(f"[reload] sample missing keys: {missing[:5]}")
if unexpected:
print(f"[reload] sample unexpected keys: {unexpected[:5]}")
# forward on the real video → expect the ramp
model.eval()
import decord
from robometer.data.dataset_types import ProgressSample, Trajectory
from robometer.evals.eval_server import compute_batch_outputs
from robometer.utils.setup_utils import setup_batch_collator
vr = decord.VideoReader("/tmp/robometer_example.mp4")
step = max(1, int(round(vr.get_avg_fps() / 3.0)))
idx = list(range(0, len(vr), step))[:10]
frames = vr.get_batch(idx).asnumpy().astype(np.uint8)
collator = setup_batch_collator(processor, tokenizer, exp_config, is_eval=True)
traj = Trajectory(
frames=frames,
frames_shape=tuple(frames.shape),
task="Pick up the object and place it in the container",
id="0",
metadata={"subsequence_length": int(frames.shape[0])},
video_embeddings=None,
)
batch = collator([ProgressSample(trajectory=traj, sample_type="progress")])
inp = batch["progress_inputs"]
for k, v in inp.items():
if hasattr(v, "to"):
inp[k] = v.to("cuda")
with torch.no_grad():
res = compute_batch_outputs(
model, tokenizer, inp, sample_type="progress", is_discrete_mode=True, num_bins=100
)
prog = np.asarray(res["progress_pred"][0], dtype=np.float32)
print(
f"[reload] progress series (meta+prequantized path): {[round(float(x), 4) for x in prog]}"
)
print(
"[reload] COMPARE to the build_experiment REFERENCE series — they must "
"match element-wise to ~1e-3 for the prequantized load to be trusted.",
flush=True,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())