WP-204 — inference, export and Python API¶
Engineering log · Phase 19 — Codebase audit remediation
🔗 Depends on WP-203 · Needed by none
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.pysplit 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 ofcoco_eval.MeanAveragePrecisionmust move with_new_metric. - Three literals stay.
_DEFAULT_TOPK = 300inmodels/heads/detect.pyandmodels/heads/obb.py, and_DEFAULT_IMG_SIZE = 640inmodels/build.py, cannot import the shared constants:models/sits belowdecode/and anylucid_yolo.decodeimport runsdecode/__init__, which importsmodels.heads.detectwhile 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); rows0..A-1are byte-identical and the rest are zero.predict_orientedandlucid-predictare unchanged on every canvas, because their confidence filter drops the padding rows. The three callers ofo2o_rotated_topkoutside this batch's files stay unpadded and unaffected. - Scoring engines differ.
score_cocodefaults tofaster_coco_evalandlucid-evaltoauto, which useshotcocowhen usable andfaster_coco_evalotherwise;score_cocoaccepts only those two names, so a Python caller passeseval_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.