From 5c53da9db7ab100df9aa4458b6e12bb14360727d Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Sat, 22 Aug 2026 10:42:54 +0900 Subject: [PATCH 1/2] fix(rt-detr-v2): make needs_sanitize independent of HashMap order `needs_sanitize` walked `weights.keys()` and returned from inside the loop as soon as either marker family was hit. `WeightMap` is a `std::collections::HashMap` and `RandomState` is seeded per map instance, so for any map carrying both families the verdict went to whichever key the randomized walk reached first, and the same checkpoint could sanitize on one run and not the next. The comment claiming an "Early out once both decisions are unambiguous" guard described a check the code never made. The wrong direction is the expensive one: a spurious `true` runs the rename and transpose pipeline over already-transposed conv weights, and the model still loads and still emits detections, they are just wrong. Accumulate both flags over every key first, then apply the precedence. MLX wins the mixed case, because that is the only recoverable direction: skipping the pipeline on something that was really raw HF fails loudly a moment later when `from_weights` looks up MLX names the map does not have, while running it over already-MLX weights corrupts silently with no downstream backstop. The mixed case now also emits a `tracing::warn!` naming the lexicographically first offending key of each family, so the ambiguity is visible to an operator without turning a heuristic that is deliberately over-broad (`.convolution.` and `.normalization.` are unanchored substring tests) into a hard load failure. The marker set, the caller, and the `bool` signature are unchanged. The trailing `has_hf_marker` return is now an explicit `false`, which is what it always evaluated to at that point. `needs_sanitize_mixed_markers_is_order_independent` builds a fresh 7-key map holding both families on each of 64 iterations, since a single call cannot distinguish a fixed verdict from a lucky one, and asserts a stable `false`. Against the unfixed function it reported `true` on 27 of 64 maps. `needs_sanitize_marker_free_maps_are_no_ops` pins the empty-map and unknown-key edge cases. Verified with `cargo test --profile test-fast --features cuda --lib vision::detection::rt_detr_v2` (32 passed), the narrower `::sanitize` filter repeated six times back to back, `cargo check --lib --tests --features cuda`, `cargo fmt --all -- --check`, and `cargo clippy --lib --tests --features cuda -- -D warnings -A clippy::err_expect`. That one allow covers a pre-existing failure at `src/multimodal/host_preprocessor_tests.rs:416` that is present on main and untouched here. Refs #1276 --- src/vision/detection/rt_detr_v2/sanitize.rs | 152 +++++++++++++++++--- 1 file changed, 134 insertions(+), 18 deletions(-) diff --git a/src/vision/detection/rt_detr_v2/sanitize.rs b/src/vision/detection/rt_detr_v2/sanitize.rs index 31a0cd127..72e62600f 100644 --- a/src/vision/detection/rt_detr_v2/sanitize.rs +++ b/src/vision/detection/rt_detr_v2/sanitize.rs @@ -43,33 +43,85 @@ const HF_PREFIX: &str = "model."; /// `decoder.layers.`; raw HF checkpoints surface `model.backbone.` / /// `backbone.model.` / `model.decoder.` and the `*.convolution.` / /// `*.normalization.` field names. We sniff a few unambiguous markers. +/// +/// The whole key set is scanned before anything is decided, and the precedence +/// is applied afterwards, so the verdict is a pure function of the key set. +/// `WeightMap` is a `HashMap` whose iteration order is randomized per map +/// instance, so deciding from inside the walk would hand the answer to +/// whichever key the walk happened to reach first. pub fn needs_sanitize(weights: &WeightMap) -> bool { let mut has_mlx_marker = false; let mut has_hf_marker = false; for key in weights.keys() { - if key.starts_with("vision.backbone.") || key.starts_with("vision.hybrid_encoder.") { - has_mlx_marker = true; - } - if key.starts_with("model.backbone.") - || key.starts_with("backbone.model.") - || key.contains(".convolution.") - || key.contains(".normalization.") - || key.starts_with("model.decoder.") - || key.starts_with("model.encoder.") - { - has_hf_marker = true; - } - // Early out once both decisions are unambiguous. - if has_mlx_marker { - return false; - } + has_mlx_marker |= is_mlx_marker(key); + has_hf_marker |= is_hf_marker(key); + } + + if has_mlx_marker { if has_hf_marker { - return true; + // Both families at once: a partially converted or hand-merged + // checkpoint, or an MLX one shipping auxiliary tensors under HF + // field names. Resolve it toward MLX instead of rejecting it, + // because only this direction is recoverable. Skipping the + // pipeline on something that was really raw HF fails loudly a + // moment later, when `RtDetrV2Model::from_weights` looks up MLX + // names the map does not have; running the pipeline over + // already-MLX weights would double-transpose the conv weights and + // leave the model quietly emitting wrong detections. The warning + // carries the diagnosis a hard error would have carried. + warn_mixed_layout(weights); } + return false; + } + if has_hf_marker { + return true; } // No markers at all: treat as already-MLX (no-op) to avoid corrupting an // unexpected layout. - has_hf_marker + false +} + +/// True when `key` carries an MLX-layout marker. +fn is_mlx_marker(key: &str) -> bool { + key.starts_with("vision.backbone.") || key.starts_with("vision.hybrid_encoder.") +} + +/// True when `key` carries a raw-HuggingFace layout marker. +/// +/// The `.convolution.` / `.normalization.` tests are unanchored substring +/// matches, so unlike the others they can fire anywhere in a key. That is what +/// lets a single map trip both families at once. +fn is_hf_marker(key: &str) -> bool { + key.starts_with("model.backbone.") + || key.starts_with("backbone.model.") + || key.contains(".convolution.") + || key.contains(".normalization.") + || key.starts_with("model.decoder.") + || key.starts_with("model.encoder.") +} + +/// Report a weight map that carries both marker families. +/// +/// Names the lexicographically first offending key of each family rather than +/// the first one the walk happens to meet, so the message is reproducible +/// across runs for the same checkpoint. +fn warn_mixed_layout(weights: &WeightMap) { + let first_matching = |matches: fn(&str) -> bool| { + weights + .keys() + .map(String::as_str) + .filter(|key| matches(key)) + .min() + .unwrap_or("") + }; + tracing::warn!( + mlx_key = first_matching(is_mlx_marker), + hf_key = first_matching(is_hf_marker), + "RT-DETRv2 checkpoint carries both MLX-layout and raw-HuggingFace key \ + markers; treating it as already-MLX and skipping the rename/transpose \ + pipeline, since re-running that pipeline would double-transpose conv \ + weights" + ); } /// True when `key` (already stripped of the `model.` prefix) should be dropped. @@ -320,6 +372,70 @@ mod tests { assert!(needs_sanitize(&hf_map)); } + /// A mixed-marker map must produce one stable verdict. + /// + /// `WeightMap` is a `HashMap` and `RandomState` is seeded per map instance, + /// not per process, so a fresh map is built on every iteration. Reusing a + /// single map would only ever exercise one iteration order and could not + /// tell a fixed verdict apart from a lucky one. + /// + /// The expected verdict is `false`: MLX wins the mixed case because + /// skipping the pipeline on a genuine HF checkpoint fails loudly at weight + /// lookup, while running it on an MLX checkpoint double-transposes conv + /// weights silently. + #[test] + fn needs_sanitize_mixed_markers_is_order_independent() { + const ITERATIONS: usize = 64; + // Three MLX-only keys, one key tripping both families at once (an MLX + // prefix that kept an HF `.normalization.` field name), and three + // HF-only keys, two of which match the unanchored substring markers. + const MIXED_KEYS: [&str; 7] = [ + "vision.backbone.embedder.embedder.0.conv.weight", + "vision.backbone.encoder.stages.0.layers.0.conv.weight", + "vision.hybrid_encoder.lateral_convs.0.bn.weight", + "vision.backbone.embedder.embedder.1.normalization.running_mean", + "model.backbone.model.embedder.embedder.0.convolution.weight", + "aux.head.0.convolution.weight", + "aux.head.0.normalization.bias", + ]; + + let mut verdicts: Vec = Vec::with_capacity(ITERATIONS); + for _ in 0..ITERATIONS { + let mut map: WeightMap = WeightMap::new(); + for key in MIXED_KEYS { + map.insert( + key.to_string(), + mlxcel_core::zeros(&[1], mlxcel_core::dtype::FLOAT32), + ); + } + verdicts.push(needs_sanitize(&map)); + } + + let sanitize_verdicts = verdicts.iter().filter(|v| **v).count(); + assert_eq!( + sanitize_verdicts, 0, + "needs_sanitize said `true` for {sanitize_verdicts} of {ITERATIONS} mixed-marker \ + maps freshly built from the same key set; the verdict must be a stable `false` and \ + must not depend on HashMap iteration order. Observed: {verdicts:?}" + ); + } + + #[test] + fn needs_sanitize_marker_free_maps_are_no_ops() { + // Empty map: nothing to sniff, so the pipeline must not run. + let empty: WeightMap = WeightMap::new(); + assert!(!needs_sanitize(&empty)); + + // Keys matching neither family: treat as already-MLX rather than + // transposing an unrecognized layout. + let mut unknown: WeightMap = WeightMap::new(); + unknown.insert( + "some.unrecognized.tensor".to_string(), + mlxcel_core::zeros(&[1], mlxcel_core::dtype::FLOAT32), + ); + assert!(!needs_sanitize(&unknown)); + } + #[test] fn maybe_transpose_conv_transposes_pytorch_layout() { // PyTorch stem conv [out=64, in=3, kH=7, kW=7]; in != kernel makes the From 576b425296fdd998ad8ee69784cb2c970886a8b0 Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Sat, 22 Aug 2026 11:06:52 +0900 Subject: [PATCH 2/2] docs(reports): add the PR #1281 technical report Bilingual report for the RT-DETRv2 layout-verdict ordering fix, recorded before the merge because this repository carries the `TECHNICAL_REPORTS/.keep-reports` marker. Force-added since `TECHNICAL_REPORTS/` is gitignored. Refs #1276 --- .../1281-needs-sanitize-order-20260822.en.md | 63 +++++++++++++++++++ .../1281-needs-sanitize-order-20260822.ko.md | 63 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.en.md create mode 100644 TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.ko.md diff --git a/TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.en.md b/TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.en.md new file mode 100644 index 000000000..2f6c09e13 --- /dev/null +++ b/TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.en.md @@ -0,0 +1,63 @@ +# Technical Report: PR #1281 - An order-independent checkpoint layout verdict for RT-DETRv2 + +## Executive Summary + +`needs_sanitize` decides whether a freshly loaded RT-DETRv2 weight map is raw HuggingFace layout and therefore needs the rename plus transpose pipeline. It walked `weights.keys()`, which is a `HashMap`, and returned from inside the loop as soon as either an MLX marker or an HF marker was seen. For a map carrying both marker families the verdict was decided by whichever key the randomized walk reached first, so the same file could be judged differently on different runs. The fix scans every key before deciding and applies an explicit precedence. + +This is the fourth instance of one root-cause class found in a single review sweep, after #1265 (test fixtures), #1267 (`lang_bias.rs`) and #1277 (distributed registry accessors). + +## 1. Problem Statement + +The loop set `has_mlx_marker` for keys prefixed `vision.backbone.` or `vision.hybrid_encoder.`, set `has_hf_marker` for six HF markers, and then returned `false` or `true` immediately. Its comment claimed it waited "once both decisions are unambiguous", which the code did not do: it returned on the first flag of either kind. + +The two directions are not symmetric in cost. A spurious `false` on a genuine HF checkpoint skips the rename, and the load then fails a moment later when the model looks up `vision.backbone.*` names the raw map does not have. A spurious `true` on an already-MLX checkpoint runs the pipeline over already-transposed weights and silently double-transposes the convolutions, which the module documentation at the top of the same file explicitly warns about. A double-transposed conv is a shape-valid tensor, so nothing downstream can flag it and the model emits confident, wrong detections. + +Reachability, stated without overclaiming. Two HF markers are unanchored substring tests, `.contains(".convolution.")` and `.contains(".normalization.")`, and a correctly converted MLX checkpoint should not trip them, because `rename_stripped` in the same file rewrites `.convolution.` to `.conv.` and `.normalization.` to `.bn.`. So for well formed inputs on both sides this was latent rather than live. It becomes reachable for a partially converted or hand merged checkpoint, an MLX checkpoint shipping auxiliary tensors under HF field names, or a future MLX layout that keeps any `.normalization.` shaped key. The point is that the function had no defense against that case and failed toward the corrupting direction at random. + +## 2. Technical Decisions + +### 2.1 Scan every key, then decide, with MLX winning + +Deciding after the full scan removes the dependence on iteration order. MLX takes precedence because it is the recoverable direction, per the asymmetry above. + +### 2.2 A warning rather than a hard error + +The issue raised whether a map carrying both families should be rejected loudly, since it is a layout the loader does not actually understand. It is not rejected, for three reasons recorded in the PR. MLX-wins is already the safe direction and the wrong guess in that direction still fails loudly a moment later at name lookup, so "silent" describes the verdict rather than the outcome. The detector is knowingly over-broad, since the unanchored `contains` tests are out of scope to change here, and making the mixed case fatal would build a hard load failure on top of a known-loose predicate. And `needs_sanitize` is `pub` returning `bool`, so a `Result` would thread a signature change through `RtDetrV2Model::load` for a case the issue itself calls latent. + +Most of the value of "loud" is recovered with a `tracing::warn!` on the mixed case naming one offending key per family. That diagnostic picks the lexicographically first match per family rather than the first the walk encounters, so the warning does not reinherit the very nondeterminism the fix removes. + +### 2.3 The misleading comment and the dead return + +The comment now describes what the code does. The trailing `has_hf_marker` return after the loop, reachable only when no key set either flag and therefore only ever `false`, no longer reads as though it could return `true`. + +## 3. Change Summary + +| File | Change | +| --- | --- | +| `src/vision/detection/rt_detr_v2/sanitize.rs` | Full scan before the verdict with explicit MLX precedence; `tracing::warn!` on the mixed case with order-independent key selection; comment corrected; dead return made explicit; 2 new tests | + +`RtDetrV2Model::load` is untouched: the signature stayed `bool`, so the call site is byte-identical. + +## 4. Review Findings + +The load-bearing requirement was proving the regression test fails against the unfixed function, because this bug class readily produces tests that pass before and after. Run against the original code, the new test reported that `needs_sanitize` returned `true` for **27 of 64** mixed-marker maps freshly built from the same key set, and printed the full 64-element verdict vector. The existing `needs_sanitize_detects_layout` passed in that same run, confirming that its green status was never evidence about this defect: it uses one single-key MLX map and one single-key HF map, so it structurally cannot observe an ordering dependence. + +The test builds a fresh `HashMap` inside each of its 64 iterations rather than reusing one, because `RandomState` randomizes per map instance and not merely per process. That property was measured separately while filing #1267: ten maps built from the same five keys produced nine distinct iteration orders inside one process. + +## 5. Validation + +Measured on GB10 (DGX Spark, CUDA sm_121, Linux aarch64). + +The branch was cut from `ca114220` and `main` had since taken #1278 and #1280, so the commit was rebased onto `bf69b83e` before gating and the gate ran on the tree that merges rather than on the stale base. After the rebase the diff against `main` is one file. + +- `make verify-test-cuda` at the rebased tree: **8229 passed, 0 failed, 311 ignored**, 101 suites, exit 0, no link or compile errors. +- `cargo test --profile test-fast --features cuda --lib vision::detection::rt_detr_v2::sanitize`: 14 passed, exit 0, and green across six consecutive runs, which is 384 independently constructed maps with no differing verdict. +- `cargo fmt --all -- --check`, `cargo check --lib --tests --features cuda`: exit 0. + +Not verified by observation: no real RT-DETRv2 checkpoint was loaded. The integration criterion holds by construction, since the signature is unchanged and only the mixed case changed behavior, but it was deliberately left unticked on the issue rather than claimed. + +## 6. Related Work + +- #1276: the issue this closes, filed from the review sweep on PR #1268. +- #1265 and PR #1266, #1267 and PR #1269, #1277: the sibling instances of the same class. +- Left alone deliberately: the `needs_sanitize` doc comment names `decoder.layers.` as an MLX marker, but the markers actually tested are `vision.backbone.` and `vision.hybrid_encoder.`. Correcting it means touching the marker set, which the issue puts out of scope. Recorded as a comment on the issue instead. diff --git a/TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.ko.md b/TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.ko.md new file mode 100644 index 000000000..8c2f78123 --- /dev/null +++ b/TECHNICAL_REPORTS/1281-needs-sanitize-order-20260822.ko.md @@ -0,0 +1,63 @@ +# 기술 보고서: PR #1281 - RT-DETRv2 체크포인트 레이아웃 판별을 순서 독립으로 + +## 요약 + +`needs_sanitize`는 갓 적재된 RT-DETRv2 가중치 맵이 raw HuggingFace 레이아웃이라 rename + transpose 파이프라인이 필요한지를 판단한다. 그런데 `weights.keys()`(즉 `HashMap`)를 돌면서 MLX 마커든 HF 마커든 **하나만 보이면 즉시** 루프 안에서 반환했다. 두 마커 계열을 모두 가진 맵은 무작위 순회가 먼저 닿은 키가 판정을 결정했고, 같은 파일이 실행마다 다르게 판정될 수 있었다. 수정은 모든 키를 훑은 뒤 명시적 우선순위로 판정하는 것이다. + +한 번의 리뷰 스윕에서 같은 근본 원인 계열로 찾은 네 번째 인스턴스다. 앞선 셋은 #1265(테스트 픽스처), #1267(`lang_bias.rs`), #1277(분산 레지스트리 접근자). + +## 1. 문제 + +루프는 `vision.backbone.` / `vision.hybrid_encoder.` 접두사에 `has_mlx_marker`를, 여섯 개 HF 마커에 `has_hf_marker`를 세운 뒤 즉시 `false` 또는 `true`를 반환했다. 주석은 "once both decisions are unambiguous"라며 둘 다 기다리는 것처럼 적혀 있었지만 코드는 어느 쪽이든 첫 플래그에서 나갔다. + +두 방향의 대가가 대칭이 아니다. 진짜 HF 체크포인트에 잘못된 `false`가 나오면 rename을 건너뛰고, 직후 모델이 raw 맵에 없는 `vision.backbone.*` 이름을 조회하면서 시끄럽게 실패한다. 반면 이미 MLX인 체크포인트에 잘못된 `true`가 나오면 이미 transpose된 가중치에 파이프라인을 다시 돌려 **conv를 조용히 이중 transpose**한다. 같은 파일 상단 모듈 문서가 명시적으로 경고하는 상황이다. 이중 transpose된 conv는 형상상 유효한 텐서라 하류에서 아무도 잡을 수 없고, 모델은 자신 있게 틀린 검출을 낸다. + +도달 가능성은 과장 없이 적는다. HF 마커 둘은 앵커 없는 부분 문자열 검사(`.contains(".convolution.")`, `.contains(".normalization.")`)이고, 제대로 변환된 MLX 체크포인트는 여기 걸리지 않아야 한다. 같은 파일의 `rename_stripped`가 `.convolution.` → `.conv.`, `.normalization.` → `.bn.`으로 바꾸기 때문이다. 그래서 양쪽 다 정상 입력이라면 live가 아니라 latent다. 부분 변환되거나 손으로 병합된 체크포인트, HF 필드명의 보조 텐서를 얹은 MLX 체크포인트, 혹은 `.normalization.` 모양 키를 남기는 미래 MLX 레이아웃에서 열린다. 요점은 그 경우에 대한 방어가 전혀 없었고, 무작위로 **손상되는 방향**으로 넘어질 수 있었다는 것이다. + +## 2. 기술적 판단 + +### 2.1 모든 키를 훑은 뒤 판정하고, MLX가 이긴다 + +전체 스캔 후 판정하면 순회 순서 의존이 사라진다. 위의 비대칭 때문에 복구 가능한 방향인 MLX에 우선권을 준다. + +### 2.2 하드 에러가 아니라 경고 + +두 계열을 모두 가진 맵은 로더가 실제로 이해하지 못하는 레이아웃이니 시끄럽게 거부해야 하지 않느냐는 논점이 이슈에 있었다. 거부하지 않기로 했고 근거 셋을 PR에 남겼다. MLX 우선은 이미 안전한 방향이고 그 방향의 오판도 직후 이름 조회에서 시끄럽게 실패하므로 "조용하다"는 것은 판정이지 결과가 아니다. 판별기 자체가 의도적으로 헐겁고(앵커 없는 `contains` 수정은 이 이슈 범위 밖), 느슨한 술어 위에 하드 로드 실패를 얹으면 오늘 잘 로드되는 체크포인트를 미래의 오탐이 거부하게 된다. 그리고 `needs_sanitize`는 `bool`을 반환하는 `pub` 함수라, `Result`로 바꾸면 이슈 스스로 latent라 부르는 사례를 위해 `RtDetrV2Model::load`까지 시그니처 변경이 번진다. + +"시끄러움"의 가치 대부분은 mixed 케이스에 `tracing::warn!`을 달아 계열별로 문제 키 하나씩을 지목하는 것으로 회수했다. 이 진단은 순회가 먼저 만난 키가 아니라 **계열별 사전순 첫 키**를 고른다. 경고 자체가 이 수정이 없애려는 비결정성을 다시 물려받지 않도록. + +### 2.3 잘못된 주석과 죽은 반환 + +주석이 이제 코드가 실제로 하는 일을 서술한다. 루프 뒤의 `has_hf_marker` 반환은 아무 플래그도 안 선 경우에만 도달해 항상 `false`인데, `true`가 나올 수 있는 것처럼 읽히던 것을 명시적으로 바꿨다. + +## 3. 변경 요약 + +| 파일 | 변경 | +| --- | --- | +| `src/vision/detection/rt_detr_v2/sanitize.rs` | 판정 전 전체 스캔 + 명시적 MLX 우선권, mixed 케이스 `tracing::warn!`(순서 독립 키 선택), 주석 정정, 죽은 반환 명시화, 신규 테스트 2건 | + +`RtDetrV2Model::load`는 손대지 않았다. 시그니처를 `bool`로 유지해 호출부가 바이트 동일하다. + +## 4. 리뷰 지적사항 + +가장 무거운 요건은 회귀 테스트가 수정 전 함수에서 실제로 실패하는 것을 증명하는 일이었다. 이 버그 계열은 수정 전후 모두 통과하는 테스트를 쉽게 만들어낸다. 원본 코드에 대고 돌리자 같은 키 집합으로 새로 만든 mixed 맵 **64개 중 27개**에서 `needs_sanitize`가 `true`를 반환했고, 64개 판정 벡터 전체가 출력됐다. 같은 실행에서 기존 `needs_sanitize_detects_layout`은 통과했다. 그 초록이 이 결함에 대한 증거가 된 적이 없음을 확인해 준다. 단일 키 MLX 맵과 단일 키 HF 맵을 쓰므로 구조적으로 순서 의존을 관측할 수 없다. + +테스트는 64회 반복마다 `HashMap`을 새로 만든다. `RandomState`가 프로세스가 아니라 맵 인스턴스마다 무작위화하기 때문이다. 이 성질은 #1267 발행 시 따로 측정했다. 같은 다섯 키로 만든 맵 10개가 한 프로세스에서 고유 순서 9개를 냈다. + +## 5. 검증 + +GB10(DGX Spark, CUDA sm_121, Linux aarch64)에서 실측. + +브랜치는 `ca114220`에서 갈라졌는데 그 사이 `main`이 #1278과 #1280을 받았다. 그래서 낡은 베이스가 아니라 **실제로 머지될 트리**를 검증하려고 `bf69b83e` 위로 리베이스한 뒤 게이트를 돌렸다. 리베이스 후 `main` 대비 diff는 파일 하나다. + +- 리베이스된 트리에서 `make verify-test-cuda`: **8229 통과, 0 실패, 311 무시**, 101 스위트, exit 0, 링크·컴파일 오류 없음. +- `cargo test --profile test-fast --features cuda --lib vision::detection::rt_detr_v2::sanitize`: 14 통과, exit 0. 연속 6회 모두 green이며, 이는 독립적으로 구성된 맵 384개에서 판정이 갈리지 않았다는 뜻이다. +- `cargo fmt --all -- --check`, `cargo check --lib --tests --features cuda`: exit 0. + +관측으로 검증하지 않은 것: 실제 RT-DETRv2 체크포인트를 적재하지 않았다. 시그니처가 그대로고 mixed 케이스만 동작이 바뀌므로 통합 기준은 구조적으로 충족되지만, 주장하지 않고 이슈의 해당 체크박스를 의도적으로 비워 두었다. + +## 6. 관련 작업 + +- #1276: 이 PR이 닫는 이슈. PR #1268 리뷰 스윕에서 나왔다. +- #1265와 PR #1266, #1267과 PR #1269, #1277: 같은 계열의 형제 인스턴스. +- 의도적으로 남겨 둔 것: `needs_sanitize` 문서 주석은 MLX 마커로 `decoder.layers.`를 적고 있지만 실제 검사하는 마커는 `vision.backbone.`과 `vision.hybrid_encoder.`다. 고치려면 마커 집합을 건드려야 하고 그것은 이슈 범위 밖이라, 이슈 코멘트로만 남겼다.