dinov2-base-onnx / leaf_kind_onnx.py
leapshared's picture
dinov2-base pooler_output ONNX(224x224, opset 20), ๋‚ด๋ณด๋‚ธ ๋„๊ตฌ, ๊ฒ€์ฆ ์ˆซ์ž
687e67b verified
Raw History Blame Contribute Delete
15.3 kB
"""DINOv2-base ํŠน์ง•(pooler_output)์„ ONNX๋กœ ๋‚ด๋ณด๋‚ด๊ณ , ๋กœ์ปฌ ๋‚˜๋ญ‡์žŽ ํŒ์ •๊ธฐ์˜ ๋‘ ๊ธธ(torchยทonnxruntime)์„ ๋…นํ™” ์‚ฌ์ง„์œผ๋กœ
๊ฒฌ์ค€๋‹ค(์ด์Šˆ #28). ๋กœ๋ด‡์— ๋ถ™์ง€ ์•Š๊ณ  GPU๋„ ์“ฐ์ง€ ์•Š๋Š”๋‹ค(CPU๋กœ ๋‚ด๋ณด๋‚ด๊ณ  ์žฐ๋‹ค).
# 1. ๋‚ด๋ณด๋‚ด๊ธฐ: torchยทtransformers์— onnxยทonnxscriptยทonnxruntime์„ ์–น์€ ํŒŒ์ด์ฌ
CUDA_VISIBLE_DEVICES= HF_HUB_OFFLINE=1 <python> scripts/diagnostics/leaf_kind_onnx.py export --out outputs/leaf_onnx/model.onnx
# 2. ๊ฒฌ์ฃผ๊ธฐ: ์œ„ ํŒŒ์ด์ฌ + ๋…นํ™” leapshared/Leaf_rules_20261002_225313_subtask์˜ ๋กœ์ปฌ ์‚ฌ๋ณธ
CUDA_VISIBLE_DEVICES= HF_HUB_OFFLINE=1 <python> scripts/diagnostics/leaf_kind_onnx.py compare --onnx outputs/leaf_onnx/model.onnx
์ €์žฅ์†Œ venv์—๋Š” onnxยทonnxruntime์ด ์—†๋‹ค. 2026-10-04์—๋Š” venv์˜ site-packages๋ฅผ .pth๋กœ ์ฝ๋Š” ์ž„์‹œ venv์—
onnx 1.23.1ยทonnxscript 0.7.2ยทonnx-ir 1.0.0ยทml-dtypes 0.6.0ยทonnxruntime 1.30.0ยทflatbuffers 25.12.19๋งŒ ๋” ๊น”์•„ ๋Œ๋ ธ๋‹ค.
export๋Š” facebook/dinov2-base(๊ณ ์ • ์ปค๋ฐ‹)์˜ pooler_output๋งŒ ๋‚ด๋Š” ๊ทธ๋ž˜ํ”„๋ฅผ ๋งŒ๋“ ๋‹ค.
- ์ž…๋ ฅ์€ pixel_values [N, 3, 224, 224] float32(์ „์ฒ˜๋ฆฌ๋ฅผ ๋งˆ์นœ ๊ฐ’), ์ถœ๋ ฅ์€ pooler_output [N, 768]์ด๋‹ค.
- 224x224์˜ ์œ„์น˜ ์ž„๋ฒ ๋”ฉ(37x37 โ†’ 16x16 bicubic)์€ torch๋กœ ํ•œ ๋ฒˆ ์…ˆํ•ด ์ƒ์ˆ˜๋กœ ๋„ฃ๋Š”๋‹ค. ๋Œ€ํ™”์ธต์€ ๋Š˜ 224x224๋ฅผ ๋„ฃ์œผ๋ฏ€๋กœ
๊ทธ๋ž˜ํ”„์— ๋ณด๊ฐ„์ด ๋‚จ์ง€ ์•Š๋Š”๋‹ค.
- ๋‚ด๋ณด๋‚ธ ๋’ค ๊ฐ™์€ ์ž…๋ ฅ์—์„œ torch์™€ ๊ฒฌ์ฃผ๊ณ , ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์›๋ณธ ๋ชจ๋ธยท์ปค๋ฐ‹ยท๋„๊ตฌ ํŒ)๋ฅผ ์ ๋Š”๋‹ค.
compare๋Š” ๋…นํ™”์—์„œ ์„œ๋น™ ์นด๋ฉ”๋ผ์ฒ˜๋Ÿผ(RGB โ†’ BGR โ†’ JPEG ํ’ˆ์งˆ 80) ์‚ฌ์ง„์„ ๋งŒ๋“ค์–ด ๋‘ ๊ธธ์— ๋„ฃ๋Š”๋‹ค.
- ๊ฒ€์ฆ ์—ํ”ผ์†Œ๋“œ 6ยท7ยท46ยท47์˜ ์นด๋“œ 22์žฅ: ๊ฒฐ์ • ํ”„๋ ˆ์ž„(ํŒ”์ด ์ถœ๋ฐœํ•˜๊ธฐ 5ํ”„๋ ˆ์ž„ ์ „)๊ณผ ๊ทธ โˆ’12ยทโˆ’6 ํ”„๋ ˆ์ž„์œผ๋กœ ์„ธ ์žฅ ํ•œ ๋ฒˆ ํŒ์ •
- ๊ฐ™์€ ์นด๋“œ๋ฅผ ๊ทธ๋ฆฌํผ๊ฐ€ ์ฅ๋Š” ํ”„๋ ˆ์ž„ 22์žฅ, ๊ทธ ์—ํ”ผ์†Œ๋“œ์˜ ์ฒซ ํ”„๋ ˆ์ž„(๋นˆ ์ค„) 4์žฅ: ํ•œ ์žฅ์”ฉ. none์ด์–ด์•ผ ํ•œ๋‹ค
- ๋‚˜๋จธ์ง€ ์—ํ”ผ์†Œ๋“œ์˜ ์นด๋“œ ๊ฒฐ์ • ํ”„๋ ˆ์ž„๊ณผ ์ฅ๋Š” ํ”„๋ ˆ์ž„: ํ•œ ์žฅ์”ฉ. ํŠน์ง•์„ ๋„“๊ฒŒ ๊ฒฌ์ค€๋‹ค
- ์ฐ๋Š” ๊ฒƒ: ์ „์ฒ˜๋ฆฌ(numpy์™€ transformers์˜ ์›์†Œ ์ฐจ์ด), ํŠน์ง• ์ฝ”์‚ฌ์ธ, ํŒ์ • ์ผ์น˜, ํŒ์ •๊ธฐ ๋งŒ๋“ค๊ธฐ, 3์žฅ ํŒ์ • ์ง€์—ฐ(JPEG ํ’€๊ธฐ ํฌํ•จ)
ํ—ˆ๋ธŒ leapshared/dinov2-base-onnx์— ์˜ฌ๋ฆฐ ์‚ฌ๋ณธ์€ export๋งŒ ์“ด๋‹ค(compare๋Š” ์ด ์ €์žฅ์†Œ์˜ dialogue/์™€ ๋…นํ™” ๋„๊ตฌ๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค).
"""
from __future__ import annotations
import argparse
import hashlib
import json
import statistics
import sys
import time
from pathlib import Path
import numpy as np
HERE = Path(__file__).resolve()
REPO = next((p for p in HERE.parents if (p / "pyproject.toml").is_file()), HERE.parent) # ํ—ˆ๋ธŒ ์‚ฌ๋ณธ์€ ์ €์žฅ์†Œ ๋ฐ–
sys.path.insert(0, str(REPO))
SOURCE = "facebook/dinov2-base"
REVISION = "f9e44c814b77203eaa57a6bdbbd535f21ede1415" # 2026-10-04 main(์ด PC HF ์บ์‹œ)
SIZE = 224
DATASET = "Leaf_rules_20261002_225313_subtask"
HELD_OUT = (6, 7, 46, 47)
KIND = {"๋ณต์—ฝ": "compound", "๋‹จ์—ฝ": "simple"}
def sha256(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(1 << 20), b""):
h.update(block)
return h.hexdigest()
def cosines(a: np.ndarray, b: np.ndarray) -> np.ndarray:
a, b = np.asarray(a, np.float64), np.asarray(b, np.float64)
return (a * b).sum(1) / (np.linalg.norm(a, axis=1) * np.linalg.norm(b, axis=1))
# โ”€โ”€ export โ”€โ”€
def export(out: Path, revision: str, opset: int | None) -> None:
import onnx
import onnxruntime as ort
import torch
import transformers
from transformers import AutoModel
torch.manual_seed(0)
model = AutoModel.from_pretrained(SOURCE, revision=revision).eval()
probe = torch.randn(3, 3, SIZE, SIZE)
with torch.no_grad():
want = model(pixel_values=probe).pooler_output # ๊ณ ์น˜๊ธฐ ์ „ ๋ชจ๋ธ์˜ ๋‹ต
class Pooler(torch.nn.Module):
"""pixel_values [N, 3, 224, 224] โ†’ pooler_output [N, 768]. ์œ„์น˜ ์ž„๋ฒ ๋”ฉ์€ 224x224๋กœ ๋ฏธ๋ฆฌ ์…ˆํ•œ ์ƒ์ˆ˜๋‹ค."""
def __init__(self, inner) -> None:
super().__init__()
self.inner = inner
embeddings = inner.embeddings
tokens = torch.zeros(1, 1 + (SIZE // embeddings.patch_size) ** 2, inner.config.hidden_size)
with torch.no_grad():
self.register_buffer("pos", embeddings.interpolate_pos_encoding(tokens, SIZE, SIZE).clone())
embeddings.interpolate_pos_encoding = lambda _embeddings, _height, _width: self.pos
def forward(self, pixel_values):
return self.inner(pixel_values=pixel_values).pooler_output
pooler = Pooler(model).eval()
with torch.no_grad():
got = pooler(probe)
print(f"์ƒ์ˆ˜ ์œ„์น˜ ์ž„๋ฒ ๋”ฉ: ์›๋ž˜ ๋ชจ๋ธ๊ณผ ์ตœ๋Œ€ ์ฐจ์ด {float((got - want).abs().max()):.3g}")
out.parent.mkdir(parents=True, exist_ok=True)
batch = torch.export.Dim("batch", min=1, max=64)
torch.onnx.export(pooler, (probe,), str(out), input_names=["pixel_values"], output_names=["pooler_output"],
dynamic_shapes={"pixel_values": {0: batch}}, opset_version=opset, dynamo=True,
external_data=False)
graph = onnx.load(str(out))
for node in graph.graph.node: # ๋…ธ๋“œ๋งˆ๋‹ค ๋ถ™๋Š” stack_trace ๋“ฑ: ๋‚ด๋ณด๋‚ธ PC์˜ ๊ฒฝ๋กœยท์ค„ ๋ฒˆํ˜ธ๋ผ ๊ณ„์‚ฐ๊ณผ ์ƒ๊ด€์—†๋‹ค. ์ง€์šฐ๋ฉด ํŒŒ์ผ์ด ๋Š˜ ๊ฐ™๋‹ค
del node.metadata_props[:]
onnx.helper.set_model_props(graph, {
"source_model": SOURCE, "source_revision": revision, "output": "pooler_output",
"input": f"pixel_values [N, 3, {SIZE}, {SIZE}] float32: AutoImageProcessor ์ „์ฒ˜๋ฆฌ๋ฅผ ๋งˆ์นœ ๊ฐ’",
"position_embeddings": f"{SIZE}x{SIZE} ์ž…๋ ฅ์œผ๋กœ ๋ฏธ๋ฆฌ ์…ˆํ•œ ์ƒ์ˆ˜",
"exported_with": f"torch {torch.__version__}, transformers {transformers.__version__}, onnx {onnx.__version__}",
"made_by": "LEAP-LAB-KUS/openarmsciedu-robot scripts/diagnostics/leaf_kind_onnx.py export",
})
onnx.save(graph, str(out))
onnx.checker.check_model(str(out))
opsets = {o.domain or "ai.onnx": o.version for o in graph.opset_import}
session = ort.InferenceSession(str(out), providers=["CPUExecutionProvider"])
for n in (1, 3):
ort_out = session.run(None, {"pixel_values": probe[:n].numpy()})[0]
diff = np.abs(ort_out - want[:n].numpy()).max()
print(f"onnxruntime {ort.__version__}, {n}์žฅ: ์ตœ๋Œ€ ์ฐจ์ด {diff:.3g}, ์ฝ”์‚ฌ์ธ ์ตœ์†Œ "
f"{cosines(ort_out, want[:n].numpy()).min():.8f}")
print(f"์ผ๋‹ค: {out} ({out.stat().st_size / 1e6:.1f} MB, sha256 {sha256(out)}, opset {opsets})")
# โ”€โ”€ compare โ”€โ”€
def serving_jpeg(image) -> bytes:
"""์„œ๋น™ ์นด๋ฉ”๋ผ(serving/cameras.py)์ฒ˜๋Ÿผ: RGB โ†’ BGR โ†’ JPEG ํ’ˆ์งˆ 80."""
import cv2
rgb = np.asarray(image.convert("RGB"), dtype=np.uint8)
ok, buf = cv2.imencode(".jpg", cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR), [cv2.IMWRITE_JPEG_QUALITY, 80])
assert ok
return buf.tobytes()
def photos(root: Path) -> list[dict]:
"""๋…นํ™”์˜ ์‚ฌ์ง„๋“ค: {"name", "kind", "group", "held_out", "jpeg"}. ๊ฒ€์ฆ ์นด๋“œ๋Š” ์„ธ ์žฅ(โˆ’12ยทโˆ’6ยท0)์ด ํ•œ ํŒ์ •์ด๋‹ค."""
sys.path.insert(0, str(REPO / "scripts" / "diagnostics"))
sys.path.insert(0, str(REPO / "scripts" / "verifier"))
import common
from leaf_view import all_cards
meta = common.episodes_meta(root)
by_episode: dict[int, list[dict]] = {}
for card in all_cards(root):
by_episode.setdefault(card["episode"], []).append(card)
rows = []
for episode, cards in sorted(by_episode.items()):
held = episode in HELD_OUT
want = {0} if held else set()
for c in cards:
want |= {c["decide"], c["close"]} | ({max(0, c["decide"] - 12), max(0, c["decide"] - 6)} if held else set())
got = common.read_frames(root, meta, episode, common.CHEST, sorted(want))
if held:
rows.append({"name": f"ep{episode:02d} ์ฒซ ํ”„๋ ˆ์ž„", "kind": "none", "group": "first", "held_out": True,
"jpeg": [serving_jpeg(got[0])]})
for c in cards:
tag = f"ep{episode:02d}#{c['card']}"
offsets = (-12, -6, 0) if held else (0,)
rows.append({"name": tag, "kind": KIND[c["kind"]], "group": "card", "held_out": held,
"jpeg": [serving_jpeg(got[max(0, c["decide"] + o)]) for o in offsets]})
rows.append({"name": f"{tag} ์ฅ ", "kind": "none", "group": "close", "held_out": held,
"jpeg": [serving_jpeg(got[c["close"]])]})
return rows
def compare(onnx_path: Path, root: Path, threads: int, repeat: int, out: Path | None) -> None:
import torch
import transformers
from dialogue.leaf_kind.leaf_kind import LeafKind
from dialogue.leaf_local import LocalLeafJudge, rgb_frame
from dialogue.leaf_onnx import OnnxLeafKind, pixel_values
classifier = REPO / "dialogue" / "leaf_kind" / "classifier.json"
torch.set_num_threads(threads)
started = time.perf_counter()
reference = LeafKind(classifier=classifier, device="cpu", threshold=0.9)
torch_load = time.perf_counter() - started
started = time.perf_counter()
candidate = OnnxLeafKind(onnx_path, classifier, threshold=0.9, threads=threads)
onnx_load = time.perf_counter() - started
rows = photos(root)
x0, y0, x1, y1 = reference.crop
pixel_diff = pixel_total = 0
feats_t, feats_o, frame_rows, judged = [], [], [], []
for row in rows:
frames = [rgb_frame(j) for j in row["jpeg"]]
for frame in frames: # ํ•œ ์žฅ์”ฉ: ์ „์ฒ˜๋ฆฌ์™€ ํŠน์ง•
crop = np.ascontiguousarray(frame[y0:y1, x0:x1])
ref_px = reference.processor(images=[crop], return_tensors="np")["pixel_values"]
pixel_diff += int((ref_px != pixel_values([crop])).sum())
pixel_total += ref_px.size
ft, fo = reference.features([frame]), candidate.features([frame])
feats_t.append(ft[0])
feats_o.append(fo[0])
pt, po = reference.proba([frame])[0], candidate.proba([frame])[0]
frame_rows.append({"name": row["name"], "kind": row["kind"], "torch": reference.classes[int(pt.argmax())],
"onnx": candidate.classes[int(po.argmax())], "dp": float(np.abs(pt - po).max())})
a, b = reference.judge(frames), candidate.judge(frames) # ๋Œ€ํ™”์ธต์ฒ˜๋Ÿผ ํ•œ ๋ฒˆ์—
judged.append({"name": row["name"], "kind": row["kind"], "group": row["group"], "held_out": row["held_out"],
"frames": len(frames), "torch": a, "onnx": b})
cos = cosines(np.stack(feats_t), np.stack(feats_o))
absdiff = np.abs(np.stack(feats_t) - np.stack(feats_o))
same_vote = sum(r["torch"] == r["onnx"] for r in frame_rows)
same_judge = sum((j["torch"]["kind"], j["torch"]["votes"]) == (j["onnx"]["kind"], j["onnx"]["votes"]) for j in judged)
print(f"ํŒ์ •๊ธฐ ๋งŒ๋“ค๊ธฐ(๊พธ๋Ÿฌ๋ฏธ๋ฅผ ๋ถˆ๋Ÿฌ์˜จ ๋’ค): torch {torch_load:.2f} s, onnx {onnx_load:.2f} s (์Šค๋ ˆ๋“œ {threads})")
print(f"์ „์ฒ˜๋ฆฌ: ๋‹ค๋ฅธ ์›์†Œ {pixel_diff}/{pixel_total}")
print(f"ํŠน์ง• {len(cos)}์žฅ: ์ฝ”์‚ฌ์ธ ์ตœ์†Œ {cos.min():.12f}(1 โˆ’ ์ฝ”์‚ฌ์ธ ์ตœ๋Œ€ {1 - cos.min():.2g}), ์›์†Œ ์ตœ๋Œ€ ์ฐจ์ด "
f"{absdiff.max():.3g}(ํŠน์ง• ์›์†Œ ํฌ๊ธฐ ์ค‘์•™ {np.median(np.abs(np.stack(feats_t))):.3g}), ์„ธ ๋ถ€๋ฅ˜ ํ™•๋ฅ  ์ตœ๋Œ€ ์ฐจ์ด "
f"{max(r['dp'] for r in frame_rows):.3g}")
print(f"ํ•œ ์žฅ ํŒ์ •(argmax) ๊ฐ™์Œ {same_vote}/{len(frame_rows)}, ํŒ์ •(kindยทvotes) ๊ฐ™์Œ {same_judge}/{len(judged)}")
def tally(select, label):
chosen = [j for j in judged if select(j)]
for path in ("torch", "onnx"):
right = sum(j[path]["kind"] == j["kind"] for j in chosen)
other = {}
for j in chosen:
if j[path]["kind"] != j["kind"]:
other[str(j[path]["kind"])] = other.get(str(j[path]["kind"]), 0) + 1
print(f" {label} {path}: ๋งž์Œ {right}/{len(chosen)}" + (f", ๋‹ค๋ฅธ ๋‹ต {other}" if other else ""))
tally(lambda j: j["held_out"] and j["group"] == "card", "๊ฒ€์ฆ ์นด๋“œ(์„ธ ์žฅ ํ•œ ๋ฒˆ)")
tally(lambda j: j["held_out"] and j["group"] == "close", "๊ฒ€์ฆ ์ฅ๋Š” ํ”„๋ ˆ์ž„")
tally(lambda j: j["held_out"] and j["group"] == "first", "๊ฒ€์ฆ ์ฒซ ํ”„๋ ˆ์ž„(๋นˆ ์ค„)")
tally(lambda j: not j["held_out"] and j["group"] == "card", "๋‚˜๋จธ์ง€ ์นด๋“œ(ํ•œ ์žฅ)")
tally(lambda j: not j["held_out"] and j["group"] == "close", "๋‚˜๋จธ์ง€ ์ฅ๋Š” ํ”„๋ ˆ์ž„")
# ์ง€์—ฐ: ๋Œ€ํ™”์ธต๊ณผ ๊ฐ™์€ ๊ธธ(JPEG ์„ธ ์žฅ โ†’ ํ’€๊ธฐ โ†’ ์ž๋ฅด๊ธฐ โ†’ ์ „์ฒ˜๋ฆฌ โ†’ ํŠน์ง• โ†’ ํŒ์ •)
three = next(r for r in rows if r["held_out"] and r["group"] == "card")["jpeg"]
latency = {}
for path, clf in (("torch", reference), ("onnx", candidate)):
judge = LocalLeafJudge(clf, path)
for _ in range(3):
judge.judge(three)
times = []
for _ in range(repeat):
started = time.perf_counter()
judge.judge(three)
times.append((time.perf_counter() - started) * 1000)
latency[path] = times
print(f"3์žฅ ํŒ์ • {path}: ์ค‘์•™ {statistics.median(times):.0f} ms, ์ตœ๋Œ€ {max(times):.0f} ms ({repeat}๋ฒˆ)")
if out:
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps({
"onnx": str(onnx_path), "onnx_sha256": sha256(onnx_path), "threads": threads,
"versions": {"torch": torch.__version__, "transformers": transformers.__version__,
"onnxruntime": __import__("onnxruntime").__version__},
"load_s": {"torch": torch_load, "onnx": onnx_load}, "pixel_diff": [pixel_diff, pixel_total],
"cosine": {"min": float(cos.min()), "median": float(np.median(cos))}, "feature_absdiff_max": float(absdiff.max()),
"frames": frame_rows, "judged": judged, "latency_ms": latency}, ensure_ascii=False, indent=1))
print("์ผ๋‹ค:", out)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
ex = sub.add_parser("export", help="dinov2-base pooler_output โ†’ ONNX(CPU)")
ex.add_argument("--out", type=Path, default=REPO / "outputs" / "leaf_onnx" / "model.onnx")
ex.add_argument("--revision", default=REVISION)
ex.add_argument("--opset", type=int, default=None, help="๋น„์šฐ๋ฉด torch ๋‚ด๋ณด๋‚ด๊ธฐ์˜ ๊ธฐ๋ณธ๊ฐ’")
co = sub.add_parser("compare", help="๋…นํ™” ์‚ฌ์ง„์œผ๋กœ torch ๊ธธ๊ณผ onnxruntime ๊ธธ ๊ฒฌ์ฃผ๊ธฐ")
co.add_argument("--onnx", type=Path, required=True)
co.add_argument("--root", type=Path, default=Path.home() / ".cache" / "huggingface" / "lerobot" / "leapshared" / DATASET)
co.add_argument("--threads", type=int, default=4, help="๋Œ€ํ™”์ธต ์„ค์ • leaf.local.threads์™€ ๊ฐ™๊ฒŒ")
co.add_argument("--repeat", type=int, default=30)
co.add_argument("--out", type=Path, default=REPO / "outputs" / "leaf_onnx" / "compare.json")
args = parser.parse_args()
if args.command == "export":
export(args.out, args.revision, args.opset)
else:
compare(args.onnx, args.root, args.threads, args.repeat, args.out)
return 0
if __name__ == "__main__":
raise SystemExit(main())