Skip to content

WP-204 — inference, export and Python API

Engineering log · Phase 19 — Codebase audit remediation

🔗 Depends on WP-203 · Needed by none

⬅️ WP-203 · Roadmap · WP-205 ➡️

WP-204 — one decode composition, one single-head model base, an export command and a Python API, consolidated in one commit

Batch 4 of the codebase-audit roadmap. The audit of 2026-10-21 found the per-task decode composed three times (prediction, evaluation, export), four near-identical single-head model classes with four deploy views, an exporter reachable only from a test, and no documented way to use a checkpoint from Python. The gate is make gate, as for every package (AGENTS.md sec. 1, D30); it will be run before the commit is made, and this page claims no result for it.

Decode. decode/per_task.py holds decode_with_coefficients, decode_with_keypoints and gather_keypoint_sets, and decode/common.py the constants MAX_DETECTIONS (300), EVAL_CONF_THRESHOLD, DEFAULT_IOU_THRESHOLD and COCO_IMG_SIZE, one gather_by_anchor that clamps a padding index and zeroes its row, and points_to_letterboxed_original. RotatedTopKDecoder pads to k rows and returns anchor indices, as TopKDecoder does. predict.py replaces four copied preambles with one _Prepared and one indexed decode; export.py and eval/coco_eval.py call the same functions, and gather_keypoints delegates to gather_by_anchor under its own parameter names, so a call by keyword still binds. A real-case oracle compared 42 outputs of random-initialised modules before and after — the four predict_* functions at two canvas sides, both decode paths and two confidence thresholds, the evaluator's batch prediction for three tasks, and the export graphs — and 41 were torch.equal; the one difference is the oriented export below.

Export. The three single-head models share a private _SingleHeadModel with one deploy() view, _DeployedO2O, in place of four _Deployed* classes; across five scales and four tasks the state-dict keys, module names, parameter counts and FLOPs are identical, and the 50 goldens pass unchanged. variant on the four task models is now str | ScaleSpec, which export.graph_from_module(module, canvas) needs to rebuild a checkpoint at its own multipliers; it checks the keys strictly, and a training-only rle_loss. prefix is the one thing it drops. lucid-export is the fifth command (cli/export.py) and needs the onnx package, which torch 2.14.1's legacy exporter imports unconditionally after the trace: with onnx hidden from the import system export_graph failed with OnnxExporterError: Module onnx is not installed!, so the dependency is the export extra and onnxruntime stays dev-only. The wheel-smoke action loops over five commands. Four export docstring paragraphs that described the pre-batch behaviour were corrected.

Python API. eval/coco_score.py factors the COCO scoring that detect_eval.run and pose_eval.run each carried into score_coco, which returns a CocoScore and prints nothing; both runners are thin wrappers, and stdout, exit code and JSON of six cases were identical before and after. The four predict_* functions take a file path or a decoded (3, H, W) tensor through one letterbox_image_tensor, and equal results were asserted for the file and both tensor forms. The package re-exports the loader, the predictors and the scorer lazily; import lucid_yolo was measured at 2062 µs and 64 modules against 164 µs and 53, with torch and Lightning still unloaded. load_eval_module reads a checkpoint once and builds the module from the payload through Lightning's public steps, so the two-read design its docstring justified is gone, and the user-facing explanation in docs/TRAINING.md is rewritten. No register row recorded that rationale. lucid-yolo fit --ckpt_path resume was tested through the command: step and epoch, the EMA shadow and count, the MuSGD momentum and the progressive-loss weight all continue.

Documentation. The training page splits into a recipe and an opt-in CUDA performance page, and gains sections on resuming, using a checkpoint from Python and exporting to ONNX. The API tabs gain an export page and the decode helpers, and inherited_members: [forward, deploy] keeps those two methods rendering under Detector, OrientedDetector and KeypointDetector after the base-class move. The README lists five commands and a "Not here, and why" table.

Limits and deferred.

  • The coco_eval.py split moved to WP-205. Separating the streaming scorers and the keypoint scoring from the bbox and segm functions was planned and not done; its test patch of coco_eval.MeanAveragePrecision must move with _new_metric.
  • Three literals stay. _DEFAULT_TOPK = 300 in models/heads/detect.py and models/heads/obb.py, and _DEFAULT_IMG_SIZE = 640 in models/build.py, cannot import the shared constants: models/ sits below decode/ and any lucid_yolo.decode import runs decode/__init__, which imports models.heads.detect while still partially initialised. Two tests pin them equal through public signatures.
  • A segmentation export still refuses canvases under 300 anchors (96 px a side and below). The shared gather would now give such a row an empty mask rather than fail, so lifting the refusal is possible; it is a behaviour decision for the principal.
  • No dynamic batch or canvas in the exported graph, and no format other than ONNX at opset 17 through the legacy exporter.
  • Tensor predict takes one image. Each image letterboxes to its own size and inverse, so a batch does not follow from the single-image path, and a batch is refused.
  • CS-10 behaviour change. The oriented ONNX graph on a canvas under 300 anchors returns (B, 300, 7) instead of (B, A, 7); rows 0..A-1 are byte-identical and the rest are zero. predict_oriented and lucid-predict are unchanged on every canvas, because their confidence filter drops the padding rows. The three callers of o2o_rotated_topk outside this batch's files stay unpadded and unaffected.
  • Scoring engines differ. score_coco defaults to faster_coco_eval and lucid-eval to auto, which uses hotcoco when usable and faster_coco_eval otherwise; score_coco accepts only those two names, so a Python caller passes eval_backend="hotcoco" explicitly to match the command.
  • Checked on CPU only for the new paths. The tensor predict input and the single-read loader were exercised on CPU, and the loader against Lightning 2.6.6, not the 2.4 floor.

No frozen golden is touched.