oo buddy boy

This commit is contained in:
firestar5683
2026-07-20 13:18:27 -05:00
parent 0eab89c8d9
commit 8b7c2b5f36
44 changed files with 1038 additions and 89 deletions
+18 -2
View File
@@ -80,6 +80,7 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument("--list", action="store_true", help="List staged models and exit.")
parser.add_argument("--force", action="store_true", help="Accepted for compatibility; selected outputs are always replaced.")
parser.add_argument("--external-gpu", action="store_true", help="Compile the driving artifact for the USB AMD GPU.")
parser.add_argument("--split-artifact", type=Path, help="Split an existing oversized PKL without compiling.")
parser.add_argument("--chunk-size-mib", type=int, default=95, help="Multipart size in MiB; must be below 100.")
parser.add_argument(
@@ -355,6 +356,7 @@ def compile_driving(
version: str,
output_dir: Path,
image_history_pipeline: str,
external_gpu: bool = False,
) -> Path:
model_type, source_args = driving_compile_args(files, input_format)
output_path = output_dir / f"{model_key}_driving_tinygrad.pkl"
@@ -390,7 +392,20 @@ def compile_driving(
]
if version:
command += ["--behavior-version", version]
subprocess.run(command, cwd=REPO_ROOT, env=build_compile_env(), check=True)
compile_env = build_compile_env()
if external_gpu:
for qcom_only_flag in ("IMAGE", "NOLOCALS", "OPENPILOT_HACKS"):
compile_env.pop(qcom_only_flag, None)
compile_env.update({
"DEBUG": "2",
"DEV": "USB+AMD:LLVM",
"WARP_DEV": "QCOM",
"FLOAT16": "1",
"JIT_BATCH_SIZE": "0",
"GMMU": "0",
})
command.append("--out-of-band")
subprocess.run(command, cwd=REPO_ROOT, env=compile_env, check=True)
return output_path
@@ -487,7 +502,8 @@ def main() -> int:
version = "v15"
version_label = version or "unspecified behavior"
print(f"Compiling {model_key} ({input_format}, {version_label}) from {args.input_dir} -> {args.output_dir}")
output = compile_driving(model_key, files, input_format, version, args.output_dir, args.image_history_pipeline)
output = compile_driving(model_key, files, input_format, version, args.output_dir,
args.image_history_pipeline, args.external_gpu)
print(f" saved {output.name}")
multipart_outputs = split_oversized_artifact(output)
if multipart_outputs:
+13 -7
View File
@@ -236,11 +236,14 @@ def compile_model(model_id: str, source: dict, version: str, workspace: Path, fo
run(["rsync", "-az", "--exclude=._*", f"{source_dir}/", f"{REMOTE}:{remote_input}/"])
log_path = workspace / "logs" / f"{model_id}.log"
command = (
f"cd {REMOTE_ROOT} && ./models --model {model_id} "
f"--input-dir {remote_input} --output-dir {REMOTE_ROOT}/compiledmodels "
f"--input-format {source['input_format']} --version {version}"
)
command_parts = [
f"cd {REMOTE_ROOT} && ./models --model {model_id}",
f"--input-dir {remote_input} --output-dir {REMOTE_ROOT}/compiledmodels",
f"--input-format {source['input_format']} --version {version}",
]
if source.get("uses_external_gpu"):
command_parts.append("--external-gpu")
command = " ".join(command_parts)
with open(log_path, "wb") as log_file:
process = subprocess.run(["ssh", REMOTE, command], stdout=log_file, stderr=subprocess.STDOUT)
if process.returncode != 0:
@@ -288,7 +291,7 @@ def validate_model(model_id: str, version: str, workspace: Path) -> dict:
return payload
def update_manifest(base_manifest: Path, workspace: Path) -> dict:
def update_manifest(base_manifest: Path, workspace: Path, source_map: dict) -> dict:
payload = load_json(base_manifest)
models = payload["models"] if isinstance(payload, dict) else payload
if not any(model.get("id") == "deeprl3v2" for model in models):
@@ -302,6 +305,9 @@ def update_manifest(base_manifest: Path, workspace: Path) -> dict:
})
multipart_handoff = []
for model in models:
source = source_map.get(model["id"], {})
if "uses_external_gpu" in source:
model["uses_external_gpu"] = bool(source["uses_external_gpu"])
artifact = workspace / "compiled" / f"{model['id']}_driving_tinygrad.pkl"
model.pop("artifact_format", None)
model.pop("artifact_size", None)
@@ -350,7 +356,7 @@ def main() -> int:
if args.command == "manifest":
if args.base_manifest is None:
parser.error("--base-manifest is required")
update_manifest(args.base_manifest, args.workspace)
update_manifest(args.base_manifest, args.workspace, source_map)
return 0
model_ids = [args.model] if args.model else list(source_map)
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import statistics
import time
from pathlib import Path
import cv2
import numpy as np
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Benchmark fixed-shape ONNX forwards through OpenCV DNN.")
parser.add_argument("models", nargs="+", type=Path, help="ONNX model paths to benchmark.")
parser.add_argument("--input-size", type=int, required=True, help="Square model input size.")
parser.add_argument("--iterations", type=int, default=50)
parser.add_argument("--warmup", type=int, default=5)
return parser.parse_args()
def percentile(values: list[float], quantile: float) -> float:
ordered = sorted(values)
index = min(round((len(ordered) - 1) * quantile), len(ordered) - 1)
return ordered[index]
def benchmark(path: Path, input_size: int, iterations: int, warmup: int) -> dict[str, object]:
net = cv2.dnn.readNetFromONNX(str(path))
blob = np.zeros((1, 3, input_size, input_size), dtype=np.float32)
net.setInput(blob)
output = net.forward()
for _ in range(max(warmup - 1, 0)):
net.setInput(blob)
output = net.forward()
durations_ms = []
for _ in range(max(iterations, 1)):
net.setInput(blob)
started_at = time.perf_counter()
output = net.forward()
durations_ms.append((time.perf_counter() - started_at) * 1000.0)
return {
"model": str(path.resolve()),
"input_size": input_size,
"output_shape": list(output.shape),
"iterations": len(durations_ms),
"median_ms": round(statistics.median(durations_ms), 3),
"p95_ms": round(percentile(durations_ms, 0.95), 3),
"mean_ms": round(statistics.mean(durations_ms), 3),
}
def main() -> int:
args = parse_args()
results = [
benchmark(path.expanduser().resolve(), args.input_size, args.iterations, args.warmup)
for path in args.models
]
print(json.dumps(results, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -86,6 +86,13 @@ def parse_read(text: str) -> tuple[str, str]:
return (speed, confidence) if separator else ("", "")
def nonzero_value(text: str) -> str:
try:
return text if float(text) > 0.0 else ""
except (TypeError, ValueError):
return ""
def queue_row(row: dict[str, str]) -> dict[str, str]:
key = record_key(row)
route, dongle_id, log_id = route_identity(row)
@@ -99,8 +106,17 @@ def queue_row(row: dict[str, str]) -> dict[str, str]:
read_sources += ";logged_vision_publish"
review_reasons = ["corrected_source_timing"]
review_priority = float(row.get("score") or 0.0)
if row.get("event_type", "") == "visionPublish":
review_reasons.extend(("route_vision_publish", f"published_{row.get('published_speed', '')}"))
published_speed = row.get("published_speed", "")
review_reasons.extend(("route_vision_publish", f"published_{published_speed}"))
if published_speed and candidate_speed and published_speed != candidate_speed:
review_reasons.append("logged_current_value_disagreement")
review_priority += 5.0
map_speed = nonzero_value(row.get("map_speed", ""))
if map_speed and published_speed and map_speed != published_speed:
review_reasons.append("logged_map_disagreement")
review_priority += 3.0
else:
review_reasons.append("route_bookmark")
@@ -129,9 +145,9 @@ def queue_row(row: dict[str, str]) -> dict[str, str]:
"read_sources": read_sources,
"read_support_count": "1",
"is_regulatory": row.get("is_regulatory", ""),
"map_current_speed_limit_mph": row.get("map_speed", ""),
"map_next_speed_limit_mph": row.get("next_speed", ""),
"review_priority": row.get("score", ""),
"map_current_speed_limit_mph": nonzero_value(row.get("map_speed", "")),
"map_next_speed_limit_mph": nonzero_value(row.get("next_speed", "")),
"review_priority": f"{review_priority:.4f}",
"review_reasons": ";".join(review_reasons),
})
return item
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from pathlib import Path
import torch
if __package__ in (None, ""):
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from common import EXTENDED_CLASSIFIER_SPEED_VALUES # type: ignore # noqa: TID251
else:
from .common import EXTENDED_CLASSIFIER_SPEED_VALUES
DEFAULT_CLASSES = tuple(str(value) for value in EXTENDED_CLASSIFIER_SPEED_VALUES) + ("reject",)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Expand a YOLO classification head while preserving existing class rows.")
parser.add_argument("--model", type=Path, required=True, help="Existing Ultralytics classification checkpoint.")
parser.add_argument("--output", type=Path, required=True, help="Expanded checkpoint path.")
parser.add_argument("--classes", nargs="+", default=DEFAULT_CLASSES, help="New output classes in dataset index order.")
return parser.parse_args()
def main() -> int:
from ultralytics import YOLO
args = parse_args()
model = YOLO(str(args.model.expanduser().resolve()))
old_names = {int(index): str(name) for index, name in model.names.items()}
new_names = dict(enumerate(args.classes))
if len(set(new_names.values())) != len(new_names):
raise ValueError("--classes contains duplicate names")
if not set(old_names.values()).issubset(new_names.values()):
missing = sorted(set(old_names.values()) - set(new_names.values()))
raise ValueError(f"Expanded classes omit existing outputs: {missing}")
head = model.model.model[-1]
old_linear = head.linear
new_linear = torch.nn.Linear(
old_linear.in_features,
len(new_names),
bias=old_linear.bias is not None,
device=old_linear.weight.device,
dtype=old_linear.weight.dtype,
)
new_index_by_name = {name: index for index, name in new_names.items()}
with torch.no_grad():
for old_index, name in old_names.items():
new_index = new_index_by_name[name]
new_linear.weight[new_index].copy_(old_linear.weight[old_index])
if old_linear.bias is not None:
new_linear.bias[new_index].copy_(old_linear.bias[old_index])
head.linear = new_linear
head.np = sum(parameter.numel() for parameter in head.parameters())
model.model.names = new_names
model.model.yaml["nc"] = len(new_names)
model.model.args["classes"] = None
output = args.output.expanduser().resolve()
output.parent.mkdir(parents=True, exist_ok=True)
model.save(str(output))
print(f"Expanded {len(old_names)} outputs to {len(new_names)}: {output}")
print(new_names)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -222,7 +222,11 @@ def save_classifier_crop(base_dir: Path, split: str, speed_value: int, image_bgr
def collect_backgrounds(background_dir: Path) -> list[Path]:
if not background_dir.is_dir():
return []
return sorted(path for path in background_dir.iterdir() if path.suffix.lower() in {".jpg", ".jpeg", ".png"})
return sorted(
path
for path in background_dir.iterdir()
if not path.name.startswith("._") and path.suffix.lower() in {".jpg", ".jpeg", ".png"}
)
def main():
@@ -233,6 +237,7 @@ def main():
parser.add_argument("--val-count", type=int, default=1200, help="Number of synthetic validation detector images.")
parser.add_argument("--negative-ratio", type=float, default=0.18, help="Share of detector images with no sign.")
parser.add_argument("--speed-values", nargs="+", type=int, default=list(DEFAULT_SPEED_VALUES), help="Posted values to synthesize.")
parser.add_argument("--regulatory-only", action="store_true", help="Generate only regulatory signs for the requested values.")
parser.add_argument("--seed", type=int, default=20260330, help="Random seed.")
args = parser.parse_args()
@@ -265,7 +270,11 @@ def main():
detector_lines: list[str] = []
if rng.random() >= args.negative_ratio:
sign_spec = choose_sign_spec(rng, speed_values)
sign_spec = (
SignSpec(detector_class=0, style="regulatory", speed_value=rng.choice(speed_values))
if args.regulatory_only
else choose_sign_spec(rng, speed_values)
)
if sign_spec.style == "advisory":
sign_image = render_advisory_sign(sign_spec.speed_value or 25, seed=rng.randint(0, 1_000_000))
else:
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import shutil
from collections import Counter
from pathlib import Path
if __package__ in (None, ""):
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from common import EXTENDED_CLASSIFIER_SPEED_VALUES # type: ignore # noqa: TID251
else:
from .common import EXTENDED_CLASSIFIER_SPEED_VALUES
IMAGE_SUFFIXES = frozenset((".jpg", ".jpeg", ".png", ".bmp", ".webp"))
SUPPORTED_CLASSES = frozenset(str(value) for value in EXTENDED_CLASSIFIER_SPEED_VALUES) | {"reject"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Merge image-folder classifier datasets using hard links when possible.")
parser.add_argument("sources", nargs="+", type=Path, help="Classifier roots containing train/ and val/.")
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--overwrite", action="store_true")
return parser.parse_args()
def link_or_copy(source: Path, destination: Path) -> None:
destination.parent.mkdir(parents=True, exist_ok=True)
try:
os.link(source, destination)
except OSError:
shutil.copy2(source, destination)
def main() -> int:
args = parse_args()
sources = [path.expanduser().resolve() for path in args.sources]
output = args.output.expanduser().resolve()
if output.exists():
if not args.overwrite:
raise FileExistsError(f"Output already exists: {output}")
shutil.rmtree(output)
counts: Counter[str] = Counter()
for source_index, source_root in enumerate(sources):
for split in ("train", "val"):
split_root = source_root / split
if not split_root.is_dir():
raise FileNotFoundError(split_root)
for class_dir in sorted(split_root.iterdir()):
if not class_dir.is_dir() or class_dir.name.startswith("._"):
continue
if class_dir.name not in SUPPORTED_CLASSES:
raise ValueError(f"Unsupported classifier class {class_dir.name!r} in {source_root}")
for image_path in sorted(class_dir.iterdir()):
if image_path.name.startswith("._") or image_path.suffix.lower() not in IMAGE_SUFFIXES or not image_path.is_file():
continue
destination = output / split / class_dir.name / f"s{source_index:02d}_{image_path.name}"
link_or_copy(image_path, destination)
counts[f"{split}/{class_dir.name}"] += 1
summary = {
"sources": [str(path) for path in sources],
"output": str(output),
"counts": dict(sorted(counts.items())),
}
(output / "merge_summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="ascii")
print(json.dumps(summary, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -21,7 +21,7 @@ VIDEO_SUFFIXES = {".hevc", ".mp4", ".mov", ".mkv", ".avi"}
def iter_source_files(paths: list[Path]):
for input_path in paths:
if input_path.is_file():
if input_path.is_file() and not input_path.name.startswith("._"):
yield input_path
continue
@@ -29,7 +29,7 @@ def iter_source_files(paths: list[Path]):
continue
for path in sorted(input_path.rglob("*")):
if path.is_file() and path.suffix.lower() in IMAGE_SUFFIXES | VIDEO_SUFFIXES:
if path.is_file() and not path.name.startswith("._") and path.suffix.lower() in IMAGE_SUFFIXES | VIDEO_SUFFIXES:
yield path
@@ -321,19 +321,32 @@ function setSpeed(speed, shouldSave) {
function handleDigitShortcut(digit) {
speedBuffer += digit;
if (speedBuffer.length > 2) speedBuffer = speedBuffer.slice(-2);
if (speedBuffer.length > 3) speedBuffer = digit;
if (speedBufferTimer) clearTimeout(speedBufferTimer);
speedBufferTimer = setTimeout(clearSpeedBuffer, 1000);
if (speedBuffer.length < 2) return;
if (speedBuffer === "10") {
clearTimeout(speedBufferTimer);
speedBufferTimer = setTimeout(() => {
if (speedBuffer === "10") {
clearSpeedBuffer();
setSpeed(10, true);
}
}, 400);
return;
}
const speed = Number(speedBuffer);
clearSpeedBuffer();
if (speeds.includes(speed)) {
clearSpeedBuffer();
setSpeed(speed, true);
return;
}
if (speedBuffer.length < 3) return;
speedBuffer = digit;
speedBufferTimer = setTimeout(clearSpeedBuffer, 1000);
}