Repository navigation
v0.33.0
Added
-
Multi-label decode on every NMS decode path (
edgefirst-decoder).DecoderBuilder::with_multi_label(true)now applies to split, quantized, 2-way, detection-only and ModelPack detection models, for bothdecodeanddecode_proto, not only fused float and per-scale models. With NMS disabled, multi-label no longer turns NMS back on. Quantized multi-label candidate selection reads the transposed score layout of a DMA-BUF output one contiguous class column at a time. -
Decoder::decode_for_trackingdecodes one label per box whatever the decoder's multi-label setting, for callers that feed an external tracker. The PythonDecoder.decode_trackeduses it. -
multi_labelin the Python decoder and the C API. EveryDecoderconstructor takesmulti_label=, andDecoder.multi_labelreports the setting. C callers set it withef_decoder_params_set_multi_labeland read it back withef_decoder_multi_label, which also reflects a model that declares it in metadata. -
Qualcomm Adreno on Linux (proprietary KGSL driver, validated on IQ-9075 / Adreno 663). The GL backend detects Adreno from
GL_RENDERER/EGL_VENDORand applies these fallbacks automatically. None of them apply on Android.- Uses the GBM EGL display; the EGL device-platform display crashes in
eglQueryStringon this driver. - Serializes GL across processors, and guards tensor
mmaps againsteglDestroyImage, which unmaps its driver mapping twice. The C API exposes this guard asef_tensor_with_cpu_mappings_excluded, so a consumer linkinglibedgefirst_tensorshares the library's single guard. - Declines DMA-BUF imports at plane offsets that are not 64-byte aligned (sources and destinations), and semi-planar sources whose chroma is a separate DMA-BUF.
- Uploads NV planes into a fresh texture rather than one that held an EGLImage.
- Creates single-channel render targets with the sized
GL_R8format. - Reports no GL float render support on a zero-copy backend, so float output goes to the CPU.
- Uses the GBM EGL display; the EGL device-platform display crashes in
-
On-demand hardware tests and benchmarks across the board fleet (
hardware-test.yml,hardware-bench.yml, bothworkflow_dispatch). One job per board entry: a runner label, or labels joined by+that one runner must all carry (imx95-frdm+ara240). The default is one board per SoC,imx8mp-evk, imx95-evk, rpi5, orin-nano, iq9075-evk. The tests workflow runs the whole on-target suite plus the C-API gates, where the CI hardware lane runs a selection; the bench workflow runs every benchmark case and summarises medians across boards, with a CSV of every median. Each summary names the runner that ran each entry. With theFLEET_RUNNERS_TOKENorganisation secret, an entry no online runner carries is reported as unavailable instead of queuing for 24 hours. -
scripts/on-target-bundle.shbuilds the self-contained bundle a board runs (tests: binaries, C-API libraries, G3 and gate sources; bench: benchmark binaries; both with merged testdata), andscripts/on-target-run.shandscripts/on-target-bench.shrun it on the board. The fleet workflows andscripts/on-target-test.shuse the same bundle and scripts.
Fixed
-
YUYV and NV12 sources in a PBO were converted on the CPU on a Jetson. Without DMA-BUF import,
create_imagebacks every source with a PBO, and the GL backend refused YUYV and NV12 from one ("PBO upload not supported", "NV R8 upload cannot map a PBO source", "Opengl doesn't support YUYV source texture"). Those are the formats cameras deliver. Where the platform has no zero-copy import, a PBO or host-memory YUYV/NV12 source now reaches the same shaders as an imported one on every route (packed, packed RGB, planar, and their int8 variants), copied on the GPU from the buffer rather than through a CPU map: 4-12x faster than the CPU converter on Orin Nano. Where the platform has zero-copy import, such sources stay on the CPU, which beats the upload there (Mesa V3D's PBO upload runs well behind it). -
A 4K frame on a Raspberry Pi 5 lost DMA-BUF. Its CMA pool is 64 MB, and a DMA-BUF allocation that CMA could not fit failed rather than trying the system heap, so
create_imagefell back to a PBO. DMA-BUF allocation now takes the system heap when the CMA heap is absent or full. System-heap memory is not physically contiguous; a consumer that cannot import it declines it as it declines any other import. -
A PBO from another processor was bound by name in the wrong GL context. GL buffer names belong to a context, and each processor has its own. Converting from or into a PBO that another processor allocated, or one wrapped with
ef_tensor_wrap_pbo, read or wrote whichever buffer had that name in this context, or failed with a GL error. Such a PBO is now read and written through its own map, which the owning processor serves. -
A planar GL convert after a packed-RGB one wrote wrong second and third planes on Vivante. The packed-RGB route left its intermediate texture bound to texture unit 1, and the planar route, which samples the same texture on unit 0 through a per-plane swizzle, then produced wrong G and B planes (off by up to 173) for every later planar convert on that processor. The packed-RGB route now unbinds it.
-
The float path's upload from host memory ignored a padded source pitch and sheared every row after the first.
-
max_detandpre_nms_top_kwere ignored on some decode paths. Flat and split YOLO detection models cappedDecoder::decodeoutput at the outputVec's capacity (300 forVec::new()) instead ofmax_det, and never appliedpre_nms_top_k. YOLO26 end-to-end models also capped at theVeccapacity, and ModelPack detection never appliedpre_nms_top_k. Every path now uses the decoder'smax_detandpre_nms_top_k; the outputVec's capacity is only an allocation hint, as documented. Thepre_nms_top_khalf of this changes detection-only output; see Changed. -
With NMS bypassed,
max_detkept the first anchors, not the best boxes (edgefirst-decoder).nms = Noneon YOLO detection and ModelPack models truncated tomax_detin anchor order. It now keeps themax_dethighest-scoring boxes and returns them in descending score order, as the NMS paths and the segmentation paths already did. -
Equal scores at the
pre_nms_top_kcut were chosen arbitrarily. The cut now keeps the lower anchor index among equal scores, so the candidates that reach NMS are reproducible against a reference decoder. -
ef_decoder_params_newdefaultedmax_detto 100 (libedgefirst_decoder), not the library default of 300 it documents and the Rust builder and Python use. It is now 300; a C caller that relied on the old value gets up to 300 detections per frame, andef_decoder_params_set_max_det(p, 100)keeps the old cap. -
Quantized 2-way YOLO segmentation models could not be decoded.
decode,decode_protoanddecode_trackedpassed the detection tensor to candidate selection without transposing it, tripping a shape assertion for any real model.decodealso left the mask-coefficient tensor untransposed, so mask generation failed a shape check whenever the anchor count differed from the prototype count. Both tensors are now transposed as in the split quantized path. -
A two-plane NV source whose GL import is refused now reports the import's reason instead of a misleading "draw_src_texture does not support Nv12".
-
ef_tensor_builder_wrapclosed the caller's fd on some failures. It adopted the handle before building the tensor, so a handlefrom_fdrefused, or a format the builder could not apply, was closed on the way out while the other failures left it open. The dynamic backend'sTensorDyn::from_fdcloses the fd itself wheneverwrapfails, and aborted on the double close ("IO Safety violation: owned file descriptor already closed").wrapnow adopts the handle only when it returns a tensor; on any failure the handle stays open and remains the caller's. -
GL convert from a host-memory source into a DMA planar destination failed on drivers that accept the destination import (Mali, V3D). The single-pass planar shader can only read a DMA source, so the convert ended with "OpenGL EGLImage requires DMA tensor, got Mem" and fell back to the CPU. A
Mem/Shmsource into a planar destination now takes the two-pass route, whose first pass uploads it. Vivante masked this: it refuses an unaligned destination import, and that refusal already re-planned the convert. -
Blob import dropped per-channel zero points. An asymmetric per-channel quantized tensor exported with
blob::exportcame back fromblob::importas symmetric. Import now rebuildsQuantization::per_channelwhen zero points are present. Quantization the header declares but cannot be rebuilt (bad axis, mismatched counts) is anInvalidArgumenterror rather than a silently unquantized tensor. -
Blob reference-mode import lost quantization on both the Unix and D3D11 paths.
-
Some valid blobs could not be parsed. The strings region was padded relative to its own start rather than the blob, so an export whose quantization arrays ended 4-byte aligned (e.g. per-tensor symmetric) failed
BlobView::parse. -
Padded planar tensors reported the wrong plane stride. For
[C, H, W]with a row stride, the descriptor, blob andef_tensor_stridesnow report[row_stride * H, row_stride, esz], agreeing with the plane table and the mapped layout. Previouslystrides[0]was the tightH * W * esz(descriptor, blob) or the row stride (C API). A padded batched[N, ...]image now stepsNover one padded image, andbatch(n)starts at that padded offset rather than atntight images, which put the element inside the previous element's padding. A single image blob refuses a batched tensor instead of describing it as one image. -
Python planar and semi-planar tensors with a padded pitch. The buffer protocol (
memoryview,np.asarray,TensorMap.numpy()) padded only the outermost stride, so a planar[C, H, W]view started each plane inside the previous plane's row padding.from_numpyassumed one padded row per pixel of height and refused padded planar tensors, and semi-planar ones whose combinedH*krows exceed the height. Both now use the same strides as the descriptor, including a one-row image whose pitch exceeds its whole logical size.from_numpyrefuses a pitch that is not a whole number of elements rather than truncating it. -
Tensor::width()/height()on a batched[N, H, W, C]image returned H as the width, which gavebatch()and strided maps the wrong pitch. They now describe one element of a batched tensor and returnNonefor ranks that are not an image (previously a panic on a flattened formatted tensor). Strided maps andcopy_to_flatof a batched packed tensor now cover allN * Hrows. -
configure_imagecould keep a stale row stride when reconfiguring to the same format, reporting a pitch below the new width's minimum. -
A reference import whose producer pitch the format refuses is now an error instead of being ignored.
-
DRM ioctl request codes are built with nix's
request_code_*macros instead of hand-rolled x86/ARM encoding.
Changed
scripts/on-target-test.shruns one invocation per host at a time. Two runs against the same host shared its remote directory and its local results directory, so one could read back the other's results: aPASSwith a fraction of the suite, as a filtered run's 31 tests were reported for another run'srpi5-hailo. Each host is now locked for the whole deploy, run and fetch, and a host another run holds is reported asBUSYwith the holder's name instead.hardware-bench.ymlgives each board 120 minutes, up from 90: the i.MX 8M Plus (Cortex-A53) took 60 minutes for every case.- Class-aware NMS is the default (
edgefirst-decoder).Nms::Autoresolves toClassAwarewhen the model config does not setnms, andNms::default()isClassAware, matching trainer validation (ModelPack, Ultralytics) and COCO evaluation. Overlapping objects of different classes (a person on a bicycle) are no longer suppressed against each other. A v1 config that says"nms": "auto"now takes this default instead of becoming class-agnostic. Configs that setnms: class_agnosticare unaffected. The C and Python ABI is unchanged but output changes:ef_decoder_params_set_nms(p, 1), the C default, andNms.Auto, the Python default, now mean class-aware NMS for a config withoutnms, so deployed ModelPack models and v1"nms": "auto"models return more boxes where objects of different classes overlap. - ModelPack detection honours the NMS mode. ModelPack detection models (flat and split) previously ran class-agnostic NMS regardless of the configured mode; they now use the decoder's resolved mode, and
nms = Nonebypasses NMS as it does for YOLO. A ModelPack config with nonmskey (older YAML/JSON configs) therefore now gets class-aware NMS; exports that declare"nms": "class_agnostic"keep class-agnostic NMS. - Multi-label decode can be declared in
edgefirst.json. The optional top-levelnms_multi_labelkey enables it for a model;with_multi_label, the Pythonmulti_labelargument andef_decoder_params_set_multi_labeloverride it. This replaces the 0.25.2 rule that multi-label was builder-only. Because a model file can now switch every untrackeddecode(includingef_decoder_decodeand the Pythondecode) to validation decode, building a decoder whose multi-label comes from metadata logs a warning namingnms_multi_label.decode_tracked,decode_proto_trackedand the Pythondecode_trackedalways decode one label per box and log a warning once per decoder when it has multi-label enabled, instead of failing adebug_assert. pre_nms_top_know caps detection-only models (edgefirst-decoder, C API, Python). Flat and split YOLO detection and ModelPack detection used to send every score-passing candidate to NMS; they now keep the toppre_nms_top_kby score first, as the segmentation paths did. With the default of 300 this drops candidates on any frame where more than 300 pass the score threshold, typically only at validation thresholds (~0.001). Setpre_nms_top_kto0(unbounded) for validation, or to at least the anchor count under argmax decode. Equal scores at the cut keep the lower anchor index.- Multi-label decode defaults
pre_nms_top_kto 30 000 (edgefirst-decoder, C API, Python), Ultralytics'max_nms, when multi-label is on (from the API ornms_multi_label) and the cap was not set explicitly; multi-label emits up to anchors × classes candidates, which 300 would truncate. An explicitwith_pre_nms_top_k,ef_decoder_params_set_pre_nms_top_kordecoder.pre_nms_top_kstill wins, and0means unbounded everywhere. The tracking entry points (decode_tracked,decode_proto_tracked,decode_for_tracking, Pythondecode_tracked) decode argmax, so they keep the 300 default unless the cap was set explicitly. In Python, assigningdecoder.pre_nms_top_kmakes the cap explicit for every decode (even if it equals a default), reading it returns the capdecodeuses, and assigningNonerestores the defaults. The newDEFAULT_PRE_NMS_TOP_KandMULTI_LABEL_PRE_NMS_TOP_Kconstants name both defaults. The C API no longer passes its own 300 to the builder, so it follows the same rule. - Multi-label decode honours an explicit class-agnostic NMS mode (
edgefirst-decoder). Multi-label decode used to switch any enabled NMS to class-aware. It now runs the decoder's resolved mode over the multi-label candidates, as Ultralyticsnon_max_suppression(multi_label=True, agnostic=True)does:with_nms(Some(Nms::ClassAgnostic)),nms=Nms.ClassAgnostic,ef_decoder_params_set_nms(p, 3)or a config's"nms": "class_agnostic"now suppresses across the classes of one anchor. With no mode set, NMS resolves to class-aware and multi-label keeps every class, as before. - Breaking Rust API changes (
edgefirst-decoder):SchemaV2andConfigOutputsgain a publicnms_multi_labelfield and are now#[non_exhaustive], so code outside the crate can no longer build them with struct literals (even with..Default::default()): start fromSchemaV2::new()/ConfigOutputs::new(outputs)/Default::default()or a parser, then assign the public fields. Later optional fields (such as atilingsection) will then not be breaking;NmsMode::from_v1returnsOption<NmsMode>(NoneforNms::Auto);Nms::default()isClassAware;Decoder::pre_nms_top_kis nowOption<usize>:None(the default) is resolved per decode by the newDecoder::pre_nms_top_k_for(multi_label)(30 000 for a multi-label decode, 300 for argmax, including tracking), andSome(n)is an explicit cap every decode uses as given (Some(0)unbounded), so code that read or assigned ausizemust readpre_nms_top_k_for(decoder.multi_label())or assignSome(n).Nms::Automust be resolved byDecoderBuilder::build: assigningSome(Nms::Auto)to the publicDecoder::nmsfield directly now trips adebug_assert!at decode in debug builds (release builds treat it as class-aware). TensorTrait::capacity_bytesandTensorTrait::set_logical_shapeno longer have default bodies; every implementor provides them.edgefirst_tensor::protocol::c_byte_stridesis public: the one stride convention shared by the descriptor, the blob and the C API.scripts/on-target-test.shdeploys one bundle and runs it on the board. Its options and summary are unchanged; the summary now also gives each host's passed, failed and ignored test totals. It armsHAL_TEST_REQUIRE_DMAwhen the board's DMA heap allocates, as the CI board lane does, so a heap that should work and does not fails instead of skipping.REQUIRE_GPU=1fails a host with no render node (NO-GPU) instead of letting every GPU test skip. A host withoutrsyncgets the bundle by tar over ssh instead of failing to sync.- CI on-target lane runs on four boards:
imx8mp-evk(Vivante),imx95-evk(Mali),rpi5(V3D) andorin-nano(Tegra). It replaces the retirednxp-imx8mp-latestlabel, which no runner answers any more. The board archive is linked against glibc 2.35 so it loads on Ubuntu 22.04 (JetPack 6). Board jobs now fail when a board with a render node cannot bring up GL (HAL_TEST_REQUIRE_GL), asscripts/on-target-test.shalready did. Tests that need a zero-copy buffer treat its absence as a failure only where one must exist: underHAL_TEST_REQUIRE_DMA, or underHAL_TEST_REQUIRE_GLon a Linux host with a DMA heap. The Jetson has no heap, so they skip there. make verify-versionruns the checkrelease.ymlandpublish.ymlgate on (.github/scripts/verify_version.py) instead of a separate, weaker shell check. The script now also checks the standalone C API crates' version requirements on the internal crates they build from, and the internal crate versions in their ownCargo.lock. A 0.x requirement matches only its own minor series, so a"0.32.0"requirement left behind by a 0.33.0 bump makes the crate unresolvable, and nothing flagged it.- Dependencies updated.
opencvmoves from 0.99 to 0.101 (the optionalopencvfeature, used only by theopencv_benchmarkcomparison benchmark). Every lockfile, including the standalone C API crates', takes the latest semver-compatible releases.
Removed
benchmark.yml. It ran four benchmarks on the i.MX 8M Plus only and scraped their text output;hardware-bench.ymlreplaces it.