Conversation
둘 다 **크래시 없이 값만 틀리던** 부류를 막는다. - `group_norm(m, x, groups, eps)` — `ggml_group_norm` 은 정규화만 하고 per-channel affine(γ·x̂+β)을 안 한다. γ=1·β=0 으로 초기화되므로 **랜덤 가중치로는 안 드러난다**(mmdet dyhead 실측 cos 0.405 → 0.999999). - `pad_reflect_ext(m, x, l0, r0, l1, r1)` — ggml 에는 `pad_reflect_1d` 밖에 없어 2D 거울 패딩을 못 쓴다. 새 커널이 아니라 **view + ggml_concat_n 조합**이다. `depend/llama/ggml` 은 건드리지 않았다. 축 하나당 out[k]=x[lo-k], out[n+lo+k]=x[n-2-k] (경계 자신은 복제하지 않음), W→H 순으로 접으면 모서리는 torch reflection_pad2d 와 같은 이중 반사가 된다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
모델 하나를 붙일 때마다 **7곳**(enum·도움말·인자파싱·전방선언·switch·본체·CMake)을
손으로 고쳐야 했다. `src/cli/inference_yolov9t.cpp` 가 그 중 CMake 한 줄을 빠뜨려
**파일 전체가 빌드에서 빠진 채** 남아 있다 — 죽은 코드인 걸 아무도 몰랐다.
g2c 생성물은 시그니처가 균일하므로(`<Arch>_forward` / `<Arch>_detect_params`)
등록으로 대신할 수 있다. 이제 모델을 추가해도 `cli.cpp` 를 안 고친다.
- `arch_registry.{h,cpp}` — 이름 → forward 표. 생성 arch 옆의 `<name>_register.cpp`
전역 객체가 스스로 등록한다.
- `draw.{h,cpp}` — 박스 + 5x7 비트맵 폰트 라벨. 기존 CLI 5개가 전부 이미지→이미지라
사각형을 그릴 일이 없어 없던 기능이다.
- `postproc` — `detect_yolo_dense()`. 기존 `gen_points`·`distance2bbox`·`nms` 를 재사용한다.
- `cli.cpp` — `run_generated()` 하나가 모든 생성 모델을 처리한다. 모르는 명령이면
레지스트리를 뒤진다. **손코딩 arch 5개는 그대로 둔다**(전처리·타일링이 제각각).
- `CMakeLists.txt` — `arch/*.cpp` glob(CONFIGURE_DEPENDS).
- `tools/install_arch.py`·`tools/example_yolo.sh` — 등록 자동화와 E2E 예제.
조용히 틀리는 걸 막은 자리 셋: gguf 의 `general.architecture` 와 명령 이름 불일치 거부,
stride 로 계산한 앵커 수와 그래프 앵커 수 불일치 거부, 박스/점수 출력을 뒤에서부터 탐색
(YOLOv10/26 은 one2many 가 앞이고 추론용 one2one 이 뒤다).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
지금까지 `VISP_BUILD` env 로만 받았다. env 는 명령줄에 남지 않아서 **어떤 라이브러리로
빌드했는지 로그에서 읽을 수 없다** — 낡은 `build/` 를 참조해 `undefined reference to visp::*`
가 났을 때 그것 때문에 원인을 찾는 데 오래 걸렸다. 인자면 명령 한 줄에 남는다.
# 전
VISP_BUILD=/path/to/dir bash tools/build/build_mmdet_cpp.sh output/MMDetBackbone
# 후
bash tools/build/build_mmdet_cpp.sh --build /path/to/dir output/MMDetBackbone
`VISP_BUILD` 도 계속 읽는다 — 인자가 우선이다. 인자를 안 주면 동작은 그대로다.
`--build` 는 위치 인자와 섞이지 않게 먼저 걷어내므로 어디에 두어도 된다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`state_dict` 키가 64자를 넘는 모델(mmdet HourglassNet 등)을 위해 128 로 올려 두었다.
그건 미루기였다:
- 더 깊은 모델이면 또 넘는다. 128 도 임의값이다.
- **링크되는 모든 소비자가 같은 값을 써야 한다.** `char name[GGML_MAX_NAME]` 이
`ggml_tensor` 의 구조체 레이아웃이라, CMake 를 안 거치는 소비자(손으로 짠 g++)가
`-D` 를 못 받으면 구조체 크기가 갈린다. 실제로 검증 러너가 그 상태였고, 그것 때문에
무관한 크래시를 ABI 탓으로 오진했다.
긴 이름은 컴파일러가 줄인다(g2c `shared/compile/tensor_names.py`) — 모듈 깊이와 무관하게
성립한다. 모듈 prefix 를 접고 접미사(`.weight`·`.running_mean`)는 보존하므로 생성 `.cpp` 의
`m["…"]` 조회와 자동으로 맞는다.
backbone.hourglass_modules.0.low2.low2.low1.0.downsample.0.weight (65)
→ backbone.hm.0.low2.low2.low1.0.downsample.0.weight (49)
실측(mmdet 100계열, 텐서명 4,051개): 축약 216개(5%), 충돌 0, 최장 63자.
되돌린 뒤 전수 재측정 **78/100 · 회귀 0**.
⚠️ g2c 의 이름 축약(Sudo42b/GTX_Compiler)과 **같이 머지돼야 한다.** 이쪽만 가면
64자를 넘는 이름이 다시 로드 거부된다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mmdet 가이드가 "tools/verify/draw_boxes.py draws such a file afterwards" 라고 안내하는데 저장소에 없었다. 검증 러너가 낸 raw float32 를 이미지에 그려 주는 스크립트다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
두 값이 코드에 박혀 있어서, 그 가정을 벗어나는 모델이 **크래시 없이 값만 틀렸다.**
`input_size` 는 640 고정이었다. 224 로 trace 한 분류기를 붙이면 그나마 시끄럽게 죽는다
(`GGML_ASSERT(ggml_nelements(a) == ne0*ne1)`). `install_arch.py` 가 생성된 `<Arch>.py` 의
trace 호출에서 실제 크기를 읽어 등록 TU 에 박는다. `--size` 로 덮을 수 있다.
정규화는 더 나쁘다. 0~1 로 고정돼 있었고 YOLO 규약이라 YOLO 에서는 안 드러났다.
torchvision 분류기는 ImageNet 통계를 쓰는데, 기본값으로 돌려도 **아무 경고 없이 다른 숫자가
나온다** — ResNet-18 실측 상대 L2 6.3e-02 이고 top-5 는 우연히 순서까지 맞았다.
그래서 "돌아가니까 맞다" 로 넘어가기 딱 좋다. `--mean/--std`(픽셀 0~255 단위)로 등록한다.
`float mean[3]` 은 등록 TU 가 `t.mean = {…}` 로 대입하므로 `std::array` 여야 한다.
백본·neck 만 g2c 로 컴파일하고, head 는 `tools/detect/head.cpp` 가 GGUF 에서 이름으로 가중치를 찾아 조립한다. 조립기 함수 13개로 dense 41계열 중 38계열이 열렸고, 그중 10계열은 코드를 한 줄도 안 쓰고 MRO 로 가장 가까운 조상의 조립기를 탄다. 검증은 512×512 · **학습 체크포인트** · torch `bbox_head` 출력 대비 **상대 L1/L2**(허용 5e-02). `tools/verify/dense_head/verify_heads.py` 가 계열 이름만 받아 잰다.⚠️ **cosine 을 쓰지 않는다.** 스케일 불변이라 크기가 통째로 틀려도 1.0 이 나온다 — `rtmdet` 이 cos 0.999450 으로 통과했는데 실제 값은 97% 틀렸다. 자를 바꾸자마자 share_conv · stride 곱 · SiLU 세 군데가 동시에 드러났다. 랜덤 초기화도 같은 이유로 못 쓴다: γ=1·β=0 이 빠진 연산을 덮어 준다. 계열마다 다른 축은 넷이고 **전부 shape 를 안 바꾼다** — 가중치 공유 · 출력 스케일 · 활성화 · 타워 구조. ggml 은 전부 통과시키므로 `type(bh)` 의 속성에서 직접 읽는다. 그래프 밖 가중치는 `append_head_weights.py` 가 덧붙인다. g2c 가 "그래프에 쓰인 가중치만 굽는" 것은 옳다 — 그 등식을 깬 건 head 를 C++ 로 뺀 이 경로다. 컴파일러에 개념을 늘리는 대신 mmdet 을 아는 쪽이 자기 것을 싣는다. `.pt` 는 클래스를 `__module__` 이름으로 절이므로 로더가 `mmdet_wrap` 을 import 할 수 있어야 한다. export 가 래퍼 모듈을 `.pt` 옆에 같이 쓴다 — 환경변수가 필요 없다. (전에는 "어디서든 로드 가능" 이라는 **주석만** 있었고 실제로는 `ModuleNotFoundError: No module named 'mmdet_wrap'` 로 죽었다.) 머지 순서: #14 → #15 → #16 → #17 → 이 PR. head.cpp 가 #14 의 `group_norm`· `argsort_top_k` 를 쓴다.
README 가 가리킬 문서가 없으면 링크가 깨지고, 문서만 있으면 아무도 못 찾는다 — 같이 넣는다. 'head 는 데이터 의존 제어흐름 때문에 trace 가 안 된다' 는 문장을 뺐다. **틀렸다** — head 도 trace 된다. C++ 로 조립한 이유는 그게 원래 설계 방향이고, 한 번 조립하면 구조를 공유하는 계열이 전부 열리며, 이음매에서 수치를 그대로 볼 수 있어서다.
영문 가이드에서만 쓰이는 도구인데 오류가 한국어로 났다. 가장 자주 보는 것은 `--name` 을 빠뜨렸을 때다 — 모델 스펙이 클래스명(`DetectionModel`)을 쓰므로 `--name Yolo26m` 으로 등록하려면 컴파일 때도 같은 이름을 줘야 한다.
같은 일을 두 문서가 다르게 시켰다 — 이쪽은 `VISP_BUILD=` 환경변수를, 배포 가이드는 `--build <dir>` 을. 둘 다 동작하지만 플래그가 지금 방식이다(명령줄만 보고 어느 라이브러리로 빌드했는지 알 수 있어야 해서 만들었다). `reg_stacked_convs` 는 `head.h:45` 에 실재하고 YOLOF 가 쓰는데 표에서 빠져 있었다.
README 에서 출발하면 yolo26m 을 못 돌린다. 모델을 넣는 방법으로 `scripts/convert.py <arch>` 만 안내하는데 그 `arch` 는 고정 목록이라 yolo26 항목이 없고, `g2c`·`install_arch.py` 는 README 에 **한 번도** 안 나온다. 진입점에서 길이 끊긴다. 표에 한 줄과 `mmdet-detectors.md` 에 짧은 절을 넣어 닫는다. 그 절의 수치는 실측이다 — yolo26m 640, ultralytics 대비 검출 3건 클래스·점수 일치(cat 0.918 · couch 0.771 · tv 0.681), 박스 0.09px 이내, 디코드 전 상대 L1 box 1.7e-03 · cls 4.7e-04.
지난 라운드에 넣은 블록이 **안 돌았다.** 맨 앞 `g2c` 가 exit 127 이다 — 상위 프로젝트의 콘솔 스크립트지 vision.cpp 것이 아니다. 게다가 그 블록이 암시하는 cwd(`vision.cpp/`)에서 `uv run g2c` 를 부르면 **거기에 빈 .venv 를 만들고** 죽는다. 실행 위치를 못박고 `uv run` 을 붙였다. 그리고 vision.cpp 트리 안에 배포 가이드로 가는 링크가 **하나도 없었다**(재귀 grep 0건). README 에서 출발한 사람은 도는 명령에 영영 도달하지 못했다. README 표와 이 문서에 링크를 단다.
두 번의 독립 실주행이 연달아 '마지막 장애물' 로 같은 것을 꼽았다 — 등록된 검출기는 그림과
개수만 내놓아서, 결과를 수치로 확인하려면 `--detect-yolo` 없이 다시 등록하고 전체를 다시
빌드하고 `postproc.cpp` 를 읽어 디코더를 밖에서 다시 구현해야 했다. 두 번 다 그렇게 했다.
좌표는 이미 `dets` 에 있었다. 원본 이미지 좌표로 되돌려 찍는다:
# x1 y1 x2 y2 score class
0 97.05 205.82 571.48 587.81 0.9184 15 cat
ultralytics 기준값과 최대 0.087px 차이다.
문서 쪽: 실행 위치가 이 파일 안에서 두 가지였다(대부분 vision.cpp 기준, 컴파일러 절만
루트 기준)는 것을 본문에 못박고, `.bin` 규약이 `run_mmdet` 것이라는 경고를 등록 절에도
둔다. README 의 가이드 링크는 단독 저장소에서 404 라 문구로 바꿨다.
- 'PYTHONPATH 에 있어야 한다' — 낡았다. export 가 래퍼 모듈을 옆에 쓰고 로더가 디렉토리를 import 경로에 넣는다(pipeline.py:490). 가이드는 이미 현행이었는데 이 문서만 옛날이었다. - `num_base` 가 'Decoding (c.det)' 표에 있었는데 실제로는 `c.head.num_base` (mmdet_to_pt.py:73-76, head.h:47). det_params 에 그 멤버가 없다 — head 표로 옮겼다. - 가이드로 가는 상대링크가 `vision.cpp/docs/...` 로 풀려 존재하지 않았다. 텍스트 경로로. - '박스 0.09px 이내' — 4차 실측 최대 0.092px. 0.1px 로. - 'Without it' 의 지시 대상이 두 문장 앞이라 오독됐다. 문장 순서 재배치.
FRCNN(two-stage) 경로를 문서대로 처음 돌리자 스크립트가 깨졌다. - 생성 .cpp 는 `visp/arch/<Arch>.h` 를 include 하는데 헤더를 그 형태로 스테이징하지 않았다(`build_mmdet_cpp.sh` 는 한다). - 옛 러너(verify/roi/, 직접 include)를 컴파일했다. 정본은 하네스가 쓰는 verify/backbone/run_frcnn.cpp (ARCH_A/B 정의 방식, verify_heads.py:445-459)다. → 하네스의 빌드 라인을 그대로 옮기고 --build/SubB arch 자동 감지를 mmdet 스크립트와 맞췄다. 실측: faster-rcnn r50(학습 체크포인트) 800×800 에서 2패스 완주, 최고 RoI 가 class 15(cat) softmax 0.893. 문서: SubB 컴파일 shape 4,256,7,7 → **1000,256,7,7** (러너가 proposal 전부를 한 배치로 넣는다 — 4 로 구우면 첫 reshape 에서 abort, 실측). run_frcnn 호출에 빠져 있던 <out_prefix> 인자와 입력(정규화 CWHN .bin)·출력(prefix 덤프) 설명을 넣었다.
'조립기 함수 4개로 8계열' · '41 중 22 커버' — 초기에 쓴 숫자다. 현행은 함수 13개, 41 중 38이 허용치 안(근거 full100_i.log), 나머지 3은 공개 체크포인트가 없는 텍스트+이미지 모델이라 측정 대상이 아니다.
atss 가 박스는 0.14px 로 맞는데 **점수만 0.071** 어긋났다. 두 원인이었다. ① mmdet 의 `test_cfg` 가 **하나도 emit 되지 않아** 라이브러리 기본값(0.05/0.5/1000/100)을 쓰고 있었다. 실측: retinanet 은 nms 0.5(기본값과 우연히 같다) 인데 **ATSS 는 0.6**, RTMDet 은 score 0.001·nms 0.65·max 300 이다. 임계값이 다르면 살아남는 박스 **집합**이 달라져 비교 자체가 성립하지 않는다. 게이트 앞으로 빼서 디코더 종류와 무관하게 나간다. ② ATSS 의 centerness 가 디코드에 안 들어갔다. mmdet 은 그것을 `score_factors` 로 받아 `_bbox_post_process` 에서 `scores *= score_factors` 를 한다 — 그래서 우리 점수가 `sigmoid(centerness)` 배만큼 높았다. **곱하는 자리가 중요하다.** mmdet 은 `filter_scores_and_topk` 로 cls 점수만 보고 자른 **뒤에** 곱한다. 앞에서 곱하면 살아남는 후보가 달라져, 점수는 맞는데 경계선 박스가 조용히 사라진다 — 짝지어 비교하면 안 보인다. YOLACT 의 coeff 갈래는 점수가 아니므로 `ctr_tanh` 로 가른다. ③ 검사기가 **개수를 안 봤다.** `match()` 가 ref→got 만 걸어서, 임계값이 어긋나 집합이 달라진 경우가 '짝지은 것들은 잘 맞음' 으로 통과했다. 실제로 이번에 개수차 2건이 그렇게 숨어 있었다. 개수 비교를 통과 조건에 넣었다. 실측 (mmdet predict_by_feat 대비, 같은 픽셀): atss 점수 0.071 → **0.000** · 박스 0.14px · 개수차 2 → **0** retinanet 0.27px · 0.000 · 개수차 0 — 회귀 없음
디코드는 **조립과 다른 축**이다. 조립은 타워 모양(head_kind)으로 갈리지만 디코드는 박스가 무엇이냐로 갈린다 — 앵커 대비 delta 냐, 격자점 대비 거리냐. 러너가 그걸 안 갈라 `detect_anchor` 하나만 불러서, 거리 계열은 앵커 파라미터가 비어 후보 0개였다. `detect_fcos` 는 이미 구현돼 있었고 아무도 안 불렀다. 부르면서 세 결함을 고쳤다: ① **centerness 를 무조건 읽었다.** GFL 은 cls 가 품질을 겸하고 VFNet 은 IoU-aware 라 그 갈래가 없다. 0 벡터를 넘기면 sigmoid(0)=0.5 라 점수가 **정확히 절반**이 된다 — 비어 있으면 곱하지 않는다. ② **격자 오프셋이 0.5 로 박혀 있었다.** FCOS 의 MlvlPointGenerator 만 0.5 이고 GFL·VFNet·RTMDet 은 AnchorGenerator(center_offset=0) 다. 틀리면 박스가 반 칸씩 밀리는데, 오차가 stride 에 비례해 커지는 것이 그 증상이다. 프론트엔드도 같이 고쳤다 — `MlvlPointGenerator` 의 속성명은 `center_offset` 이 아니라 **`offset`** 이라(point_generator.py:107), 하나만 보면 point 계열이 전부 0.0 으로 나갔다. ③ **점수를 임계값 전에 곱했다.** mmdet 은 `filter_scores_and_topk` 로 cls 만 보고 자른 뒤 centerness 를 곱한다. 앞에서 곱하면 살아남는 후보가 달라진다. 그리고 `norm_on_bbox` 재적용을 없앴다 — 조립기가 이미 픽셀 거리로 만들어 놓는다. 실측 (mmdet predict_by_feat 대비, 같은 픽셀 512): fcos 0.13px · 0.000 (이전: 박스 0개) gfl 0.23px · 0.004 (이전: 박스 0개) vfnet 0.46px · 0.003 (이전: 박스 0개) atss 0.14px · 0.000 회귀 없음 retinanet 0.15px · 0.006 회귀 없음 TOOD 소경로도 넣었다(격자 좌표 ×stride, cls 는 이미 확률이라 시그모이드 재적용 금지 — mmdet tood_head.py:546 과 같다). 다만 **검증은 못 했다**: tood 는 cls 는 torch 와 맞는데 box 갈래가 상대 L1 0.63 으로 어긋난다 — 조립 문제라 디코드 범위 밖이다. RTMDet 도 같은 부류다(레벨 1·2 의 cls 가 torch 와 크게 다르다).
`detect_detr` 도 이미 구현돼 있었고 아무도 안 불렀다. DETR 계열은 early return 조차 없어서 `detect_anchor` 로 떨어졌고, 앵커 파라미터가 비어 후보 0개였다. 배선하면서 고친 것: - **출력 인덱스가 FPN 레벨이 아니라 decoder 층이다**(head.h:172-175). mmdet 도 `all_layers_*[-1]` 만 쓴다 — 앞 층을 쓰면 shape 가 맞아 안 죽고 더 거친 박스가 나온다. query 수는 설정이 아니라 **텐서에서 다시 확인**한다(어긋나면 조용히 잘못 읽는다). - **`use_sigmoid` 를 잘못 읽고 있었다.** DETR 계열은 `use_sigmoid_cls` 속성이 **없어서** 기본값 True 로 떨어졌다 — 고전 DETR 은 softmax 인데 sigmoid 로 디코드될 뻔했다. 정본은 `loss_cls.use_sigmoid` 다(detr_head.py:114). - **`num_classes` 가 채널 폭이었다.** softmax head 는 배경을 한 칸 더 써서 `cls_out_channels = num_classes + 1` 이다. 채널 폭과 의미상 클래스 수를 갈랐다 — 뒤바꾸면 배경을 클래스로 세거나 마지막 클래스를 잃는다. - `detect_detr` 에 이미지 클램프를 넣었다(mmdet 도 자른다). - 레벨 수 가드와 레벨별 수집을 DETR 에서 건너뛴다. 검사기도 둘 고쳤다: - DETR 은 transformer 가 head 가 아니라 **detector** 에 달려 있어 `bbox_head(feats)` 로 못 부른다 — 그런 계열은 `det.predict()` 를 탄다. - 매처가 **라벨을 안 봤다.** 한 물체에 두 클래스가 겹쳐 나오면(DETR 이 흔하다) 두 ref 가 같은 got 에 붙어 '라벨 불일치' 로 잘못 보고됐다 — DINO 가 그렇게 0.109 로 실패했다. 같은 라벨 안에서 먼저 짝짓게 고치니 0.030 · 불일치 0. 실측 (mmdet 대비, 같은 픽셀 512): conditional_detr 0.27px · 0.002 dab_detr 0.28px · 0.001 dino 0.41px · 0.030 detr 1.85px · 0.044 기존 5계열 회귀 없음 deformable_detr 은 **검증 못 했다** — head 출력이 다르다(우리 cls 최대 sigmoid 0.366 vs mmdet 0.833). tood·rtmdet 과 같은 부류로, 조립 문제라 디코드 범위 밖이다.
run_frcnn 이 SubB 의 원시 텐서만 덤프하고 detect_roi 를 안 불렀다. RoI head 출력을 softmax 로 확률화한 뒤(detect_roi 는 softmax 완료본을 기대한다 — postproc.h:150; SubB 는 로짓이다, 행 합 -0.41 로 실측) 클래스별 delta 디코드와 NMS 를 태운다. 파라미터는 frcnn.json 에 이미 다 있다(rcnn_means/stds/ rcnn_score_thr/rcnn_nms_thr/rcnn_max/class_agnostic) — 프론트엔드 변경 없음. 출력 규약은 run_mmdet 과 같게 맞춘다: <prefix>.boxes.bin 은 박스당 6값 (x1,y1,x2,y2,score,class) + 좌표 표 출력. 실측(faster-rcnn_r50_fpn_1x_coco, cat-and-hat.jpg 800, mmdet predict 와 같은 픽셀): 박스 0.10px · 점수 0.0008 · 라벨 불일치 0 · 개수차 0 (4/4건) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
YOLOXHead 는 타워 조립이 anchor 와 같아 head_kind::anchor 로 실렸는데, 디코드는 전혀 다르다 — 박스 코더가 없고 격자 단위 (dx,dy,log w,log h) 를 내며 점수는 cls×obj 다. anchor 로 두면 Delta 디코더로 떨어지고, 앵커 파라미터가 안 실린 계열이라 후보 0개가 나왔다. 조립축과 디코드축을 가른다: head_kind::yolox 를 새로 두되 조립은 anchor 와 같은 tower_head_forward 를 탄다. objectness 는 out.ctr 로 온다(프론트엔드가 conv_obj 를 centerness_head 로 싣는다) — 레벨 수가 모자라면 조용히 넘기지 않고 멈춘다. verify_postproc.py: **정규화를 안 하는 계열**을 다룬다. YOLOX 의 DetDataPreprocessor 는 mean/std 자체가 없어 AttributeError 로 죽었다. 없으면 0/1 로 본다 — 파라미터 헤더가 싣는 값과 같아야 양쪽이 같은 픽셀을 본다. 실측(yolox_s_8xb8-300e_coco, cat-and-hat.jpg 512, mmdet predict_by_feat 대조): 박스 0.41px · 점수 0.002 · 라벨 불일치 0 · 개수차 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## seesaw_loss (64.13px → 0.09px) 두 가지가 겹쳐 있었다. ① **분류기가 `NormedLinear`** — 가중치와 입력을 각각 L2 정규화하고 온도 20을 곱한다. 가중치 정규화는 추론에서 **상수**라 프론트엔드에서 미리 접어 평범한 Linear 로 만든다. 남는 입력 정규화는 `Tensor.norm` 이 trace 에 안 잡히므로 `sqrt(sum(x*x))` 로 풀어 쓴다. 온도를 빼먹으면 크래시 없이 점수만 틀린다 — bias 가 뒤에 더해져 상쇄도 안 되고 softmax 는 스케일 불변이 아니다. ② **점수 활성이 softmax 가 아니다** — `SeesawLoss.custom_activation` 이다. 채널이 `num_classes + 2`(앞 C 개 = 클래스, 뒤 2개 = 전경/배경)이고, 각각 softmax 한 뒤 클래스 점수에 전경 확률을 곱하고 배경 확률을 뒤에 붙인다.⚠️ 채널 수를 **출력 크기 −1 로 유추하면 안 된다** — 그러면 클래스가 하나 많아지고 라벨이 통째로 한 칸 밀린다. 객체(`bh.num_classes`)에서 읽는다. ## crowddet (109.71px · 1:500 → 0.07px · 1:1) 네 겹이었고 **전부 RPN 설정이 기본값이 아니어서** 생긴 것이다. 넷 다 shape 를 안 바꾼다. ① **set-NMS** — 같은 proposal 에서 나온 상자끼리는 서로 안 누른다. 겹쳐 선 두 사람은 IoU 가 높아 보통 NMS 면 하나가 지워진다. 이걸 빼면 억제가 통째로 어긋난다(1:500). 정제 분기가 있으면 **정제된 쌍**(cls_ref/box_ref)으로 예측한다. ② **`centers=[(8,8)]×5`** — mmdet `AnchorGenerator` 는 centers 가 주어지면 그 값을 쓰고 `center_offset` 을 무시한다. 재현식으로 만들면 stride 64 레벨에서 크게 어긋난다. ③ **`clip_border=False`** — proposal 을 이미지로 자르지 않는다(800 입력에 1054 가 나온다). ④ **`use_sigmoid` 미지정 → objectness 가 2채널** — RPNHead 의 기본은 1채널 sigmoid 지만 `loss_cls` 에 `use_sigmoid=True` 를 안 적으면 2채널 softmax 가 된다. 전경은 **index 0** 이다(mmdet v2.0 이후 FG label = 0). 2채널을 1채널로 읽으면 앵커 절반의 배경 로짓을 다른 앵커의 전경 점수로 오해한다 — 이게 제일 컸다(19.61px → 0.07px). RPN **텐서**는 처음부터 rel_L1 3e-04 로 맞았다. 그래서 그래프가 아니라 호스트 디코드로 범위를 좁힐 수 있었다 — 값이 틀리면 텐서부터 배제하는 것이 항상 빠르다. `min_bbox_size`(NMS 앞 필터)도 같이 넣었다. 다섯 파라미터 전부 기본값이 현재 동작과 같으므로 다른 계열은 그대로다: faster_rcnn 0.10px · cascade_rcnn 0.09px · ms_rcnn 0.07px.
`../../../../../GTX_Compiler/...` 는 이 트리에서 /tmp/GTX_Compiler 로 풀린다. 하네스는 `os.path.exists` 로만 보고 없으면 넘어가므로 **꺼진 줄 모르고** 돌았다 — 트래커·반지도 래퍼 config(ByteTrack 등)가 그만큼 INIT_FAIL 로 빠졌다. 도구를 상위 저장소(test_script/mmdet/)로 옮겼으니 저장소 내부를 가리킨다.
…으로 내림 전수 회귀(40계열, 새 workdir): PASS 26/40 at 800, + fpg(1024) = 27. 기존 통과 계열의 수치는 전부 그대로다 — 오늘 바꾼 공유 코드(RPN 파라미터 5개 · conv_transpose 3겹 · 하네스 stale 제거)에 회귀가 없다. - **추가 3**: crowddet 0.07px · ms_rcnn 0.07px · seesaw_loss 0.09px. - **내림 1**: panoptic_fpn. `panopticapi` 미설치로 이 환경에서 **재측정이 안 된다**. 기록돼 있던 0.04px 는 export 가 실패한 실행에서 나왔을 가능성이 크다 — 하네스가 `frcnn.json` 존재만 검사해서 지난 실행의 파일이 통과를 냈다. 지금은 단계마다 지운다. 틀렸다고 단정하지 않되 **검증됨으로 세지 않는다**. - **grid_rcnn 을 별도 항목으로** 세웠다. "디코더가 없다" 와 "연산이 없다" 를 같은 줄에 쓰지 않는다 — 디코드는 다 짰고 grouped conv_transpose 하나가 막고 있다. - 실패 분석 산문에서 '계열 전용 후처리' 항목이 비었다. 셋 다 **텐서에는 안 보이던** 차이였다는 점을 남긴다 — crowddet 은 RPN 텐서가 처음부터 3e-04 로 맞았고, 그래서 호스트 코드로 범위를 좁힐 수 있었다.
getting-started · overview · using-the-cli · using-the-library 는 docs/library-guides 브랜치(2026-08-07)에만 있었다. staging 의 docs 는 mmdet-detectors.md · model-implementation-guide.md 둘뿐이었다. README 는 통째로 가져오지 않았다 — 그 브랜치의 README 는 mmdet 항목이 생기기 전 것이라 표에서 MMDetection 줄을 지운다. 링크 줄만 더했다. 브랜치 정리(2026-08-18) 전 흡수. 원본은 archive/docs-library-guides-20260818.
…ernet one-stage 전수 회귀 34계열: **32 PASS**. 기존 26계열 수치는 전부 그대로다. ## 20260512-e7 세션이 만든 것 (onestage-decoders + unary-contig 패치) - `detect_anchor` 확장(시그니처 불변) — softmax head · 레벨별 앵커 · TBLR 코더. fsaf 0.56px. - `detect_paa` 신설 — top-k 가 (앵커,클래스)가 아니라 **앵커 단위**, 점수가 sqrt(cls×iou), NMS 뒤 score voting. 판정은 이름이 아니라 `with_score_voting` **속성** 으로 한다(LAD 는 PAAHead 를 상속해서 이름으로 가르면 빠진다). paa 0.54 · lad 0.74px. - `detect_yolact` 신설 — fast NMS. test_cfg 가 `nms` dict 대신 평평한 `iou_thr`/`top_k` 다. - `min_bbox_size` — NMS **앞** 필터. 없으면 SSD 가 경계 클램프로 면적 0 이 된 유령 상자를 31건 낸다(IoU 0 이라 NMS 도 못 지운다). mmdet 4건 vs C++ 31건 → 4 vs 4. ssd 0.42px. - `level_anchors()` — 재현식이 안 통하는 생성기는 `pg.base_anchors` 를 그대로 싣는다 (YOLACT 는 base_size 와 중심을 stride 와 따로 준다 → 재현식은 0.859배 작고 반 칸 밀린다). - `detect_corner` 신설 — 코너 heatmap 국소최대 → 코너별 top-k → 쌍 → soft-NMS. emb 거리(CornerNet)와 centripetal 판정 둘 다. cornernet 0.03px. - 단항 활성이 view 입력을 받으면 `ggml_is_contiguous_1` 로 죽는 것 — relu·sigmoid 입력에 `ggml_cont`. centernet DCNv2 의 `sigmoid(slice(conv))` 가 그 사례다. - **하네스 구멍**: 빌드 실패해도 지난 바이너리가 남아 PASS 를 냈다. 빌드 전에 지운다. ## 이 세션이 더한 것 - **`detect_centernet` 신설** — 히트맵 국소최대 → 전체 top-k → 같은 자리의 wh/offset 으로 상자. mmdet 은 `with_nms=False` 가 기본이라 NMS 를 안 건다. centernet 0.13px.⚠️ maxpool 의 패딩은 **-inf** 다. 0 으로 채우면 경계에서 음수 값이 최대가 못 되어 중심점이 사라진다 — 범위 밖을 아예 안 보게 짰다. heat 는 head 안에서 이미 sigmoid 를 거쳤다(또 걸면 안 된다). - 하네스가 `img_meta['border']` 를 채운다. CenterNet 의 `_predict_by_feat_single` 이 그걸 읽는데(RandomCenterCropPad 가 남기는 값) 없으면 **기준값 쪽이** KeyError 로 죽는다. 정사각 리사이즈에는 여백이 0 이라 항등이다 — 계열이 안 되는 게 아니라 못 재던 것이다. ## 판정에 못 미친 둘 (통과로 세지 않는다) - `yolact` 0.74px · **개수차 1** — mmdet 0.3013 vs C++ 0.2951 로 하네스 임계 0.30 경계. free_anchor·double_heads 와 같은 부류다. 박스는 맞는다. - `centripetalnet` 3.80px — 2건 중 1건의 tl 코너가 인접 셀 하나 차이. mmdet 자신의 `_decode_heatmap` 을 우리 텐서에 돌려 **공식은 동치**임을 확인했다(0.4387 vs 0.4393). 남은 것은 heatmap 근소 값차가 top-k 순위를 뒤집는 fp16 타이 플립이다.
전수 회귀: one-stage 34계열 중 32 PASS · two-stage 40계열 중 26(+fpg 1024) = 27. 기존 계열 수치는 양쪽 다 그대로다. 추가 6: fsaf 0.56 · paa 0.54 · lad 0.74 · ssd 0.42 · cornernet 0.03 · centernet 0.13px. 여섯 전부 "범위 밖" 으로 적혀 있던 것이고 하루에 닫혔다 — **"전용 디코더가 필요하다" 는 "못 한다" 가 아니다.** 필요했던 것은 그 계열의 config 를 기본값이라 가정하지 않고 읽는 것뿐이다. 판정에 못 미친 둘은 별도로 적었다. `yolact`(0.74px, 개수차 1)는 하네스 임계 0.30 경계 아티팩트라 free_anchor·double_heads 와 같은 칸이고, `centripetalnet`(3.80px)은 mmdet 자신의 `_decode_heatmap` 을 우리 텐서에 돌려 **공식 동치**를 확인했으므로 디코드가 아니라 정밀도다. 하네스가 못 잰 것과 대상이 못 하는 것을 같은 칸에 쓰지 않는다.
grid head 의 deconv 가 `groups=9, padding=1` 이라 ggml 의 `ggml_conv_transpose_2d_p0`(padding 0 · groups 1 전용)로는 못 돌렸다. 정의대로 쪼갠다: 그룹마다 커널(ne3 = IC 축)과 입력(ne2 = 채널 축)을 view 로 잘라 따로 돌리고 채널로 이어붙인다. padding 은 앞서 넣은 크롭이 처리한다.⚠️ 안 쪼개고 그냥 넘기면 **groups 가 조용히 무시되어 채널이 섞인 채 돈다.** 크래시가 없으므로 렌더러에서 막아 뒀었는데, 이제 지원하니 가드를 풀고 인자를 넘긴다. ## clamp 하나에 1.30px 디코드를 다 맞추고도 1.30px 이 남았고, **전부 이미지 경계에 걸친 상자**였다 (mmdet 801.3 vs 우리 800.0). mmdet 은 자르는 것처럼 보인다: bboxes[:, [0, 2]].clamp_(min=0, max=img_meta['img_shape'][1]) 그런데 `bboxes[:, [0, 2]]` 는 **팬시 인덱싱이라 복사본**이다 — `clamp_` 는 그 복사본을 자르고 버린다. 즉 mmdet 은 **자르지 않는다.** 우리도 안 자르게 바꾸니 0.15px. 라이브러리의 **실제 동작**이 정본이지 코드가 표현하려던 의도가 아니다. 원식을 옮길 때 "이 줄이 정말 효과가 있나" 를 한 번 물어야 하는 자리가 있다. 회귀 7/7: ms_rcnn 0.07 · crowddet 0.07 · seesaw 0.09 · faster_rcnn 0.10 · htc 0.12 · scnet 0.18px. centernet(one-stage, deconv 를 타는 다른 계열)도 0.13px 그대로다.
two-stage 28계열(26 at 800 + fpg 1024 + grid_rcnn). 회귀 7/7 통과. '연산 부족' 항목이 비었다 — grouped conv_transpose 를 그룹별 분해로 지원한다. 안 맞는 계열은 12개, 네 갈래다: 표준 앵커 RPN 이 아님(5) · fp16(3) · 경계 아티팩트(double_heads) · 기타(fast_rcnn 가중치 없음 · panoptic_fpn 환경 부족 · tridentnet 원인 미규명).
"러너가 박스를 0건 낸다" 로 오래 남아 있던 계열이고, 원인은 둘이었다. 둘 다 C4 구조 (neck 없음 · 단일 레벨 stride 16)에서만 드러난다. ① **한 레벨에 스케일이 5개다.** FPN RPN 은 레벨마다 스케일이 하나라 `scales[0]` 만 실어도 됐지만, C4 는 `scales=[2,4,8,16,32]` 를 **한 레벨에** 준다. 첫 개만 쓰면 앵커가 15개가 아니라 3개가 되고, RPN cls 채널 15개를 3개로 읽어 **엉뚱한 자리를 objectness 로 오해한다** — proposal 이 전부 빗나가 최종 박스가 0건이 됐다. 실측으로 rpncls 가 50×50×**15**임을 확인하고 짚었다. 목록 전체를 싣고 base_size 는 stride 로 둔다(mmdet `base_sizes` 기본값). 스케일이 하나인 계열은 결과가 완전히 같다 — faster_rcnn 0.10px 그대로. ② **proposal 이 상한보다 적게 나온다.** SubB 는 상한(rpn_max=1000)으로 구워지고 그 안의 flatten 이 `ggml_reshape_2d(…, 2048, 1000)` 으로 **행 수를 상수로 박는다.** 적게 넣으면 `ggml_nelements(a) == ne0*ne1` 로 죽는다. FPN 계열은 후보가 많아 항상 상한을 채우지만 C4 는 레벨이 하나라 NMS 뒤 107개만 남았다. 상한까지 0 으로 채우고 **디코드는 앞 M_real 행만** 쓴다. ①을 고치자 크래시 지점이 ②로 옮겨갔다. 첫 수정 뒤에도 실패하는 것은 다음 원인이 있다는 뜻이지 앞 수정이 틀렸다는 뜻이 아니다 — 오늘만 세 번째다(convT · crowddet · 여기).
two-stage 29계열. '미분류' 로 남아 있던 tridentnet 이 C4 전용 두 겹(한 레벨 5스케일 · proposal 이 상한 미달)이었음을 적었다. 안 맞는 계열은 11개, 네 갈래다.
커버리지 정산에서 나왔다. 표들은 하네스가 '돌리는' 74계열을 분류하는데 configs/ 아래는 100이다. 차집합 26 중 24는 정당한 제외였고(트래커 7·마스크 전용 5· 텍스트 조건부 3·이미 분류 5·reid·백본 예제 2) condinst·boxinst 둘만 이유가 없었다. 둘 다 이미 verify_heads.py 손목록에 있었고 체크포인트도 받아져 있었다 — 아무도 안 돌렸다. condinst 는 코드 변경 0으로 통과한다. CondInstBboxHead 가 FCOSHead 를 상속하고 박스 경로를 안 건드린다. controller conv(169채널)와 param_pred/points/strides 는 전부 마스크 갈래로 간다. 하네스가 kind fcos 로 자동 판정했다. boxinst 는 BoxInstDataPreprocessor.__init__ 의 무조건 raise 에 막혔다. skimage 를 실제로 쓰는 자리는 if training: 안 하나뿐이라 추론엔 안 쓴다 — 계열 한계가 아니다. panoptic_fpn 문단은 '환경' 대신 '인터프리터'로 고쳤다. venv 가 둘이고 상보적으로 깨져 있어 어느 파이썬이냐가 답을 결정한다. 지연 검사라 import 로는 못 가른다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
'디코드 전에 이미 갈린다' 로 묶여 있던 셋을 컴파일러 수정 뒤에 다시 쟀다. 둘은 코드 변경 없이 그냥 통과했다 — tood 0.63px · deformable_detr 0.26px. 기록된 실패는 그때의 나무지 지금의 나무가 아니다. dyhead 는 여전히 안 맞지만 자리가 다르다. 실제 이미지에서 층별로 재니 넥 2e-03 · head 1e-04 로 맞고 박스만 112px 어긋난다(점수는 네 자리까지 동일). 러너의 head 덤프를 mmdet predict_by_feat 에 그대로 먹이면 mmdet 박스가 나온다 — 호스트 디코더 말고는 남는 자리가 없다. coder 상수·strides· center_offset·octave_base_scale 은 config 와 일치하고 같은 앵커 생성기를 쓰는 atss 는 0.14px 로 통과한다.⚠️ 손으로 재잴 때 짝을 조심하라. tood 를 처음 재쟀을 때 13.89px 가 나왔는데 결함이 아니라 내 짝짓기 실수였다 — verify_heads 는 손목록의 anchor-free config 로 내보내는데 mmdet_families.resolve() 는 anchor-based 를 준다. 짝은 하네스에서 받아야 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scikit-image 설치가 전부였다. 코드는 한 줄도 안 고쳤다 — BoxInstBboxHead 는 CondInstBboxHead 의 디코더를 재정의 없이 그대로 탄다. 막고 있던 건 BoxInstDataPreprocessor.__init__ 의 무조건 raise 였고, skimage 를 쓰는 자리는 if training: 안 하나뿐이라 추론은 거기 닿지도 않는다. 학습 전용 의존성에 걸린 생성자 가드는 결과표에서 '미지원 아키텍처' 와 똑같이 보인다 — 둘은 다른 칸에 적어야 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
panopticapi 를 하네스 인터프리터에 넣고 재측정하니 0.04px 로 예전 기록과 같은 값이 나왔다. 그 숫자는 stale 산출물로 통과한 run 에서 나온 것이라 '미확인' 으로 내려 뒀었는데, '미확인' 과 '틀림' 은 다른 주장이고 측정에서 살아남은 건 하나뿐이다. 안 재고 지웠으면 맞는 숫자를 버릴 뻔했다. 막고 있던 건 '환경' 이 아니라 '인터프리터' 였다. venv 두 개가 상보적으로 깨져 있어(한쪽은 mmdet.models 가 죽고 다른 쪽은 panopticapi 가 없다) 두 세션이 같은 패키지를 두고 정반대 결론을 냈다. 둘 다 자기 인터프리터에 대해서는 옳았다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
트래커·반지도 래퍼는 검출기를 model.detector 안에 넣고 자기는 껍데기만 갖는다. config 를 벗기는 단계는 dense_head 하네스에만 있었고 roi 하네스엔 없어서 'ConfigDict has no attribute backbone' 으로 export 에서 죽었다. **config 만 벗기면 안 된다.** 저장 접두사는 config 키가 아니라 래퍼가 만든 속성 이름이다 — 트래커는 detector., SoftTeacher 는 self.student/self.teacher 두 벌을 만들어 semi_test_cfg.predict_on 이 고른다(teacher.). 틀리면 한 텐서도 안 실리는데 load 는 조용히 성공하고 랜덤 가중치로 그래프가 구워진다 (562px · 라벨 91건 · 검출 100건=상한).⚠️ dense_head 하네스가 정확히 그 상태였다. config 만 벗기고 체크포인트는 그대로 넘겨서, 기준값도 같은 bb.pt 에서 나오는 탓에 **랜덤끼리 일치해 PASS** 가 떴다. soft_teacher L1 1.04e-03 PASS 인데 체크포인트 일치는 0/348 이었다. 지금은 348/348 이고 L1 6.45e-03 이다 — 나빠 보이는 쪽이 정직한 숫자다. 현재 표의 계열 중엔 래퍼가 없어 발표된 수치는 무사하지만, 티켓 006(YOLOX 트래커)이 이 경로를 탄다. 접두사가 안 맞을 때는 둘을 갈라야 한다. mmdet 은 트래커용으로 **검출기만** 배포하기도 한다 — deepsort·sort·strongsort 는 이미 평평하다(backbone./neck./…). 'backbone.' 유무로 '이미 평평함'과 '추측이 틀림'을 가르고, 후자는 raise 한다. 8계열 전부 확인: 5개 벗김 · 3개 원본 그대로 · raise 0건. 그 밖에 - two_stage_families 가 래퍼 한 겹 안을 본다(40→45). 안 그러면 통과한 계열이 회귀 검사를 못 받는다. 안 재는 것이 실패보다 위험하다 - unwrap 을 in-process 로 (서브프로세스면 import mmdet 5.95초가 계열마다 붙는다) - 벗긴 .pth 를 fr/ 에 두고 조기 반환에서도 지운다 (계열당 150~500MB) - meta 보존 — init_detector 가 dataset_meta 를 읽고 없으면 COCO 80 으로 조용히 되돌아간다 (YTVIS 40 · 보행자 1 계열이 어긋난다) 회귀: two-stage 40계열 판정 변화 1건(panoptic_fpn 은 panopticapi 설치로 열린 개선) · one-stage 5계열 L1 동일 · 벗긴 .pth 잔여 0개. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
004 로 경로는 뚫렸는데 deepsort 가 **어느 이미지에서도 0건**이었다. 사진 문제가 아니라 트래커 6계열이 data_preprocessor 를 **래퍼에만** 두기 때문이다 (soft_teacher 만 안쪽에 있다 — 하필 004 로 관통시킨 그 계열이 예외라 모른 채 넘어갈 뻔했다). 벗기면 정규화가 사라져 mean 0 / std 1 로 돈다. 내리면서 타입을 DetDataPreprocessor 로 바꾼다. 원본은 TrackDataPreprocessor 라 비디오 배치를 기대하는데, 벗긴 config 는 평범한 FasterRCNN 이다. 지금 경로는 det.predict 로 건너뛰어 안 터지지만 inference_detector 로 열면 죽는다. 학습 전용 batch_augments 도 뗀다. 계열별 시험 이미지는 mmdet_families.test_image() 에 뒀다(두 하네스 공유). MOT 트래커는 보행자 1클래스라 고양이 사진에서 0건이 나온다. 새 자산은 안 들였다 — 이미 있던 bench-image.jpg 로 다섯 계열 2~5건이 나온다. 결과(bench-image.jpg): deepsort 0.04 · sort 0.04 · qdtrack 0.03 · masktrack_rcnn 0.09 · bytetrack 0.06 · ocsort 0.15 · strongsort 0.25px. 입력 크기 1440x800 은 필요 없었다 — YOLOX 는 완전 합성곱이라 512 로 돈다.⚠️ 정정: 앞선 커밋에서 '양쪽 0건이면 개수차 0 으로 조용히 통과한다' 고 적었는데 **틀렸다.** match() 가 한쪽이 비면 None 을 돌려 EMPTY 로 보고된다. 실제 대가는 헛통과가 아니라 '못 잼, 다만 보임' 이다. 주석·문서를 정정했다. 리뷰 반영: test_image 경로를 저장소 tests/input 에 고정(사용자 --image 디렉토리에서 찾으면 엉뚱한 곳을 가리킨다) · 존재 검사 후 폴백 · 명시적 --image 는 계열별 지정을 무시하고 존중 · 배너가 '계열별 지정 적용' 을 밝힘 · one-stage 하네스는 덮어쓰지 않고 권장 이미지와 다르면 경고. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gen_anchors 는 중심을 center_offset * base_size 로 잡는데, 호출부가 base_size = stride * octave_base_scale 을 넘기고 있었다. mmdet AnchorGenerator 는 base_sizes = [min(stride)…] 로 두고 octave_base_scale 은 scales 쪽에 넣는다. 앵커 **크기**는 어느 쪽으로 접든 같아서 shape 검사에 안 걸린다. **중심**만 달라진다: stride 32 · obs 8 이면 mmdet 16 vs 우리 128 = 정확히 112px. center_offset 이 0 인 계열(retinanet·atss·gfl…)은 양쪽 다 0 이라 안 드러난다. 전 계열을 훑어 노출된 것은 dyhead·glip 둘뿐이다 — 계열 하나가 앵커 디코더 전체의 결함을 혼자 지고 있었던 셈이다. 회귀: atss 0.14 · fsaf 0.56 · ghm 0.46 · ssd 0.42 · yolof 0.12 · retinanet 0.27px — 전부 기록값과 소수점까지 동일. 그리고 짝짓기 가드를 넣었다. 오늘 두 번(tood 13.89px · retinanet 20.02px) 손으로 고른 짝 때문에 멀쩡한 코드를 결함으로 오해했다. verify_heads 가 used.json 에 구울 때 쓴 config·checkpoint 를 남기고, verify_postproc 가 읽어 다르면 경고한다. retinanet r18 vs r50-caffe 로 실제 잡히는 것을 확인했다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
solo·solov2·maskformer·mask2former·mask2former_vis 가 HEAD_NONE 으로 떨어지고 있었는데 실패가 아니라 **분류가 없었던 것**이다. head 이름이 bbox_head 가 아닐 뿐 (mask_head·panoptic_head·track_head) 백본+넥은 bb.pt 로 이미 잘 나왔다. 이들에게 박스는 판정 기준이 될 수 없으므로(애초에 안 낸다) 그래프 출력을 torch 와 직접 댄다. 러너는 이미 있던 run_dump.cpp 를 그대로 쓴다 — head·decode 없이 out_* 만 덤프한다. 붙는 자리는 head_type==raw → two-stage 실패 → no-box. solov2 6.30e-04 · solo 6.99e-04 · mask2former_vis 1.87e-03 maskformer 1.94e-03 · mask2former 2.19e-03⚠️ 이건 **컴파일 범위(백본+넥)** 가 맞다는 뜻이지 마스크 head 까지 검증됐다는 뜻이 아니다. 마스크 head 는 C++ 로 안 옮겼고, maskformer 계열은 neck 도 없어 컴파일 범위가 백본뿐이다. 문서에 그렇게 적었다 — 하네스 한계를 대상 한계로 적지 않는 것과 같은 이유로, 그 반대(한계를 성과로 적는 것)도 하지 않는다.⚠️ 처음엔 넷 다 rel L1 1.5~2.0 이 나왔다. torch 는 CHW, 러너는 HWC 인데 안 맞춘 것이다 (1.4~2.0 은 무관한 두 텐서의 값이다). 축을 맞추니 6e-04~2e-03. reid 는 no-box 가 아니라 mmpretrain 의존 문제라 012 로 옮겼다. 가중치는 자기 metafile 에 없고 트래커 config 의 reid.init_cfg 가 가리켜서, 받아 손목록에 등록했다. 회귀: retinanet·fcos·atss·yolox·detr·tood·condinst·swin 8계열 L1 전부 동일. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fast_rcnn 은 proposal 을 밖에서 받는 것이 **정의**다. rpn_head 도 test_cfg.rpn 도 없어서 export 가 AttributeError 로 죽고 있었는데, 미지원이 아니라 다른 계약이다. SubA 가 RPN 없이 feats 만 내고, 러너는 FRCNN_PROPOSALS 파일에서 proposal 을 읽는다. 기준값도 **같은 파일**을 읽어 roi_head.predict 에 직접 넣는다 — 각자 만들면 proposal 생성기를 재게 된다. proposal 은 **고정 8×8 격자 64개**(사용자 결정). RPN 출력을 쓰면 백본·넥·RoI head 가 faster_rcnn 과 전부 같아져 'faster_rcnn 을 다른 이름으로 재는 것'이 된다. 격자는 결정적이고, RoIAlign+RoI head 를 proposal 생성기와 분리해서 본다.⚠️ 개수는 계약이다. SubB 가 proposal 행 수를 그래프에 상수로 굽는다(flatten 이 reshape 로 박힌다) — frcnn_wrap.EXTERNAL_PROPOSAL_COUNT 를 하네스가 읽어 정확히 그만큼 만든다. 처음엔 rpn_max=0 으로 둬서 러너가 죽었다. 그리고 짝 목록을 mmdet_families.OVERRIDE 로 옮겨 **두 하네스가 같은 것을 보게** 했다. 예전엔 one-stage 는 손목록, two-stage 는 metafile 이라 같은 계열이 다르게 풀렸다 — 오늘 그것 때문에 두 번(tood 13.89px · retinanet 20.02px) 멀쩡한 코드를 결함으로 봤다. 회귀: two-stage 45계열, 기존 38계열 판정 변화 0건. PASS 28 → 35 (늘어난 7 = 래퍼 5 + fast_rcnn + panoptic_fpn, 전부 이번 작업분). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
groie 는 RPN·RoI head 가 둘 다 표준이고 extractor 만 다르다. 레벨을 고르는 대신 **전 레벨에 RoIAlign 을 걸고** 레벨마다 5x5 conv+ReLU 를 태운 뒤 더한다. conv+ReLU 가 비선형이라 '합쳐서 한 번' 으로 접을 수 없다. 그래프 입력이 하나뿐이므로 Double-Head 와 같은 수법을 썼다 — 러너가 레벨별 RoI feature 를 배치로 이어붙이고, SubB 가 그 안에서 pre→합산→post 를 한다. 레벨 수·레벨당 RoI 수는 export 시점 상수로 박는다(n_half 와 같은 이유 — shape 에서 뽑으면 렌더러가 슬라이스 시작을 몰라 offset 0 으로 폴백한다). roi_align_params.force_level 레벨을 고르지 않고 강제 (-1 이면 평소대로) frcnn.json groie_levels 러너·하네스가 배치 배수를 안다 SubB groie_pre/post pre 는 레벨 공통이라 (L*N) 배치에 한 번만 생성 코드 확인: sl14 / sl15(offset 1000) / sl17(2000) / sl19(3000) 을 차례로 더한다 — 이 부분은 정확히 렌더링된다. WARN 아직 통과 못 한다. 남은 벽은 extractor 가 아니라 GeneralizedAttention(post)의 위치 임베딩 broadcast 가 5차원이라는 것이다. ggml 은 4D 까지라 렌더러가 ggml_cont 로 떨어뜨리고(생성 코드에 repeat [1000,6,7,4,42] 주석이 남는다) 뒤의 add 가 GGML_ASSERT(ggml_can_repeat) 로 죽는다. 이건 하네스가 아니라 컴파일러(렌더러) 작업이다. 회귀: faster_rcnn 0.10 / htc 0.12 / fast_rcnn 0.01px 동일, double_heads 도 기록대로. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0.03px (83계열)
GARPNHead(앵커 모양 예측)·CascadeRPNHead(다단계 정제)는 호스트 rpn_proposals 로
앵커를 못 깐다. 그런데 **RoI head 는 표준**이라 proposal 만 밖에서 받으면 나머지
경로는 그대로 잰다 — 거절하는 대신 외부 proposal 계약으로 돌린다.
proposal 은 그 계열 **자신의 rpn_head** 가 torch 에서 낸 것을 쓴다. fast_rcnn 처럼
고정 격자를 쓰면 안 된다 — 거긴 RPN 이 아예 없어서 낼 주체가 없었고, 여긴 있다.
frcnn.json 의 own_rpn 이 그 둘을 가른다.
WARN 이 숫자는 'RPN 까지 검증됐다'가 아니다. RPN 은 torch 에서 돈다 —
검증된 것은 백본·넥·RoIAlign·RoI head·디코드다. 문서에 그렇게 적는다.
MaskedConv2d 를 CPU 등가로 대체했다. GARPNHead 가 쓰는데 CUDA 커널만 있어
masked_im2col_forward_impl 로 죽는다. 이 연산은 정확도가 아니라 속도 최적화라
(마스크 1인 자리만 계산, 나머지 0) dense conv * mask 와 값이 같다.
CascadeRPNHead 는 meta 에 pad_shape 를 요구한다 — 다른 RPN 은 안 읽어서 여태 없었다.
WARN 도중에 내 원칙을 내 코드가 어겼다. 기준값이 proposal 파일을 안 읽고 자기 RPN 을
다시 돌려 **양쪽이 다른 proposal 로 재고** 있었다(7.22px). rpn_head 유무로 가르던
조건을 '파일이 있으면 무조건'으로 고치니 0.02px.
sparse_rcnn/queryinst 은 이 경로로 안 열린다 — test_cfg 가 {rcnn: {max_per_img: 100}}
뿐이고(score_thr·NMS 없음) SparseRoIHead 6단계 DIIHead 가 DETR 처럼 디코드한다.
새 조립기가 필요하다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
비표준 RPN 을 외부 proposal 로 돌리면서 NotImplementedError 가드를 통째로 걷어냈다.
그 결과 회귀에서 셋이 나빠졌다:
groie UNSUPPORTED(이유 명시) -> RUN_FAIL(스택 주소만)
sparse_rcnn/queryinst UNSUPPORTED(이유 명시) -> EXPORT_FAIL(no attribute 'score_thr')
이 저장소가 바로 그 자리에 적어 둔 원칙을 어긴 것이다 — "AttributeError 로
흘려보내지 말고 왜 안 되는지 말한다. 그래야 미지원과 우리 버그가 구분된다."
외부 proposal 로 돌릴 수 있는 건 **RoI head 가 표준일 때뿐**이다:
GARPNHead / CascadeRPNHead RPN 만 다르다 -> 외부 proposal 로 열림
EmbeddingRPNHead 디코드 규약이 다르다 -> 거절 (SparseRoIHead 6단계
DIIHead, score_thr/NMS 없이 DETR 식 top-k)
groie 도 집계 경로는 되지만 post 의 GeneralizedAttention 이 5D broadcast 라
못 굽는다 — 러너 크래시로 흘리지 말고 여기서 말한다. 렌더러 작업이지 하네스 작업이 아니다.
그리고 grounding_dino 계열의 num_cp(fairscale gradient checkpointing)를 끈다.
학습용 메모리 최적화라 추론값이 안 바뀐다 - SyncBN->BN 과 같은 부류다.
끄고 나니 텍스트 3계열이 전부 같은 벽에 도달한다: transformers 미설치.
회귀: PASS 28 -> 37. 바뀐 7계열은 전부 이번 작업분
(cascade_rpn/guided_anchoring/fast_rcnn/panoptic_fpn 통과, groie/sparse_rcnn/queryinst 는
위 거절 복구로 UNSUPPORTED 유지).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sparse_rcnn/queryinst 정찰 결과, 막힐 줄 알았던 자리가 안 막힌다.
EmbeddingRPNHead 가중치 두 개뿐 (init_proposal_bboxes 100x4,
init_proposal_features 100x256) — 이미지와 무관한 상수다
DIIHead (bbox_feats 100x256x7x7, object_feats 1x100x256)
-> cls 1x100x80 / bbox 1x100x4 / object_feats 1x100x256
DynamicConv 이름과 달리 conv 가 아니라 torch.bmm 이다. 가중치가 데이터에서
나오지만 연산은 표준 배치 행렬곱이라 정적 그래프로 표현된다
래퍼는 Double-Head 와 같은 수법을 쓴다 — 그래프 입력이 하나뿐이라 RoI feature 와
query 를 배치로 이어붙여 받고 안에서 가른다. 출력은 셋(cls/bbox/object_feats)이고
object_feats 를 러너가 다음 단계로 실어 나른다. proposal 수는 export 시점 상수다.
검증 두 단계:
1) torch 대조 — cls/bbox/object_feats 세 출력 모두 최대차 0.00e+00
2) g2c 컴파일 — 통과. 40 텐서 25.2MB, unhandled op 0,
TODO 는 레이아웃 주석 하나(다른 통과 계열에도 있는 그것)
DynamicConv 의 bmm 이 ggml_mul_mat 으로 정상 렌더
즉 groie 와 달리 **렌더러 벽이 없다.** 남은 것은 러너 쪽이다 —
6단계 루프(박스+object_feats 를 함께 실어 나른다)와 DETR 식 최종 디코드
(sigmoid -> query x class top-k, NMS 없음).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sparse_rcnn/queryinst 의 배관을 전부 붙였다. 러너가 6단계를 완주하고 100건을 낸다.
아직 통과는 아니다(우리 최고점 0.2356 vs mmdet 0.8068).
붙인 것
SPARSE_SubB RoI feature + query 를 배치로 이어붙여 받고(Double-Head 수법),
cls/bbox/query 셋을 낸다. torch 대조 최대차 0.00e+00
frcnn_to_pt SparseRoIHead 면 단계마다 SPARSE_SubB 를 저장
frcnn.json sparse_stages · num_proposals · rcnn_clip_border 추가
proposal 수는 rpn_max 가 아니라 학습된 query 개수(100)다
run_frcnn.cpp query 를 단계 사이로 나른다(뒤 M행의 (0,0) 자리에 심고
세 번째 출력에서 받는다) + DETR 식 최종 디코드(sigmoid -> top-k,
NMS 없음. test_cfg.rcnn 에 score_thr 도 NMS 도 없다)
FRCNN_DEBUG=1 단계별 cls/query 를 찍는다 — 이 문제를 좁힌 도구다
좁혀진 것 (다음 세션은 여기서 시작하면 된다)
query 나르기는 **정상이다** — 단계별 |mean| 이 torch 와 거의 일치
(0.7844/0.7670 · 0.7946/0.8053 · 0.7995/0.7995 · 0.7887/0.7754)
RoIAlign 파라미터도 일치 (out 7 · strides [4,8,16,32] · sampling 2 · aligned)
clip_border 도 반영했다 (sparse 는 False — 자르면 다음 단계가 다른 feature 를 읽는다)
그런데 **stage 0 부터 다르다**: 우리 sigmoid 0.5368 vs torch 0.6739.
단계가 갈수록 벌어진다(0.179 / 0.220 / 0.246 / 0.329 / 0.236 vs torch 0.65~0.81).
래퍼는 torch 에서 정확히 일치했으므로 **굽고 난 그래프가 torch 와 다르다**는 뜻이다.
다음 할 일: 컴파일된 SPARSE_SubB 를 동일 입력으로 torch 와 직접 대조한다.
후보 — fp16 가중치(gguf 가 fp16 이다) · attention/bmm 렌더링.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
glip / grounding_dino / mm_grounding_dino 가 열렸다. glip L1 2.60e-03 grounding_dino L1 2.54e-03 mm_grounding_dino L1 2.15e-03 계획은 이것을 "BERT 인코더를 새로 들이는 큰 일(5~10일)" 로 잡았는데 틀렸다. 백본이 Swin 이라 이미 지원되고(swin 0.22px), 언어 모델은 torch 쪽에서 돈다. 실제로 막던 것은 둘뿐이었다: 1) num_cp=6 (fairscale gradient checkpointing) — 학습용이라 추론 무관, 껐다 2) transformers 미설치 WARN 이 셋은 bbox_head(feats) 로 못 부른다 — 텍스트 임베딩이 함께 들어가야 한다 (ATSSVLFusionHead.forward() missing 1 required positional argument). 박스를 판정 기준으로 삼을 수 없으므로 박스 없는 계열과 **같은 자**로 잰다: 컴파일된 그래프를 torch 와 댄다. config 에 language_model 이 있으면 no-box 경로로 간다. 즉 이 숫자는 "이 계열이 검증됐다" 가 아니라 "컴파일 범위가 맞다" 는 뜻이다. 설치는 하네스 venv 에 했다. 앞서 "6계열이 위험한 설치 하나에 걸려 있다" 고 적었는데 과했다 — 2026-08-03 사고는 mmpretrain 이 blip->transformers 를 끌어온 것이고, 이 venv 엔 mmpretrain 이 없어 그 경로가 안 열린다. transformers 단독은 다른 일이다. 버전 핀을 세 번 맞췄다(transformers<5 · tokenizers 0.22.2 · huggingface-hub<1.0). 검증: 기존 패키지 버전 변화 0건 · numpy 2.5.1 그대로 · import mmdet.models OK. 회귀: retinanet/fcos/atss/yolox/detr/tood/condinst/swin/solo/maskformer 10계열 L1 전부 동일. 남은 3 - convnext / timm_example / reid 는 mmpretrain 이 필요하다(격리 venv 필요). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FRCNN_DEBUG=2 로 stage 0 의 입력과 출력을 덤프해 torch 와 직접 대조했다.
같은 입력에서 torch cls max 0.7256 (sigmoid 0.6738)
그래프 cls max 0.1475 (sigmoid 0.5368) 최대차 6.67
torch 가 러너의 입력만으로 mmdet 의 stage-0 값(0.6739)을 **정확히 재현한다.**
따라서 무죄로 갈린 것:
- RoIAlign feature (러너가 만든 그대로 넣어 맞았다)
- query packing (뒤 100행의 (0,0) 자리에 심는 계약이 맞다)
- 가중치 (fp16 왕복 0.7261 vs fp32 0.7256 — fp16 이 원인이 아니다)
- 그래프 구조 (sl6=feature 는 52줄, sl7=query 는 21줄에서 각각 쓰인다)
남은 것은 **컴파일된 그래프의 수치**뿐이다. g2c 쪽 조사라 지시대로 여기서 멈춘다.
groie(5D broadcast)와 같은 칸에 들어간다.
다음 사람이 이어갈 지점: FRCNN_DEBUG=2 로 덤프를 만들고
SPARSE_SubB 를 torch 로 돌린 값과 중간 텐서를 층별로 좁힌다.
후보는 attention(MultiheadAttention)과 DynamicConv 의 bmm 두 곳이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
신규 클론에서 vision.cpp 빌드 전에 하네스를 돌리면 계열마다 `BUILD_FAIL … ld returned 1 exit status` 만 나온다. 링커는 무엇이 없는지 못 말하므로 저장소가 깨진 것으로 읽힌다 — 실제로 그렇게 읽었다. 두 하네스 모두 시작 전에 libvisioncpp.so / libggml.so 를 확인하고, 없으면 찾은 경로와 빌드 명령을 찍고 멈춘다.
서브에이전트로 문서 9건을 코드와 대조해 나온 것들. - overview.md 가 visp/nn.h · postproc.h · tracker.h 를 공개 헤더처럼 표에 실었다. 셋 다 src/visp/ 에 있고 include/ 에 없다 — 설치본에 링크한 프로그램은 include 못 한다. 설치되는 4개(vision·image·ml·util)와 in-tree 3개를 표를 갈라 구분했다. - model-implementation-guide.md 의 ml.hpp · image.hpp · vision.hpp → 전부 .h 다. - mmdet-detectors.md: 본문이 41+38 인데 표는 40+35 다. 표는 **엄격 기준을 통과한 것만** 싣고 나머지(free_anchor·yolact 경계, fp16 3계열, double_heads)는 뒤 절 산문에 있다. 총합 86이 맞아떨어져서 아무도 안 봤다 — 표가 검증 집합이 아니라는 것을 명시했다. - README 의 esrgan 예제가 빌드가 받지 않는 파일명을 썼다. models/CMakeLists.txt 및 나머지 문서와 같은 RealESRGAN-x4plus_anime-6B-F16.gguf 로 통일. - vision-cpp-mmdet-guide-en.md: 3장이 mmdet 에 적용 안 된다는 예외를 맨 앞으로, 도구가 둘(vision-cli/run_mmdet)이라는 사실을 해상도 문단에 명시, mmdet_wrap 사본을 .pt 옆에 쓴다는 잘못된 서술 정정, --name 대소문자 규칙 명문화.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
무엇
mmdet 계열 검증을 0 → 86계열까지 올린 작업 전부. 커밋 93개.
어떻게 쟀나
학습된 체크포인트로, 양쪽에 같은 픽셀을 넣어 mmdet 자신의
predict_by_feat(two-stage 는 detector 의
predict)와 대조한다. 하네스는tools/verify/dense_head/verify_postproc.py와tools/verify/roi/verify_postproc_roi.py.주의해서 읽을 것
glip · grounding_dino · mm_grounding_dino)의 숫자는 "컴파일 범위가 맞다" 는 뜻이지
"그 계열이 검증됐다" 가 아니다. 마스크 head 와 텍스트 인코더는 C++ 로 안 옮겼다.
res2net)의 숫자를 fp32 로 대체하지 마라. 두 열을 따로 적는다.
UNSUPPORTED). 크래시나 조용한 오답이 아니다.클론해서 바로 돌리려면
depend/llama는 upstream(c66ee8e,GGML_MAX_NAME=64)을 가리킨다 — 의도한 것이다.로컬 패치 커밋으로 포인터를 올리면 클론한 사람이 그 커밋을 못 받는다(원격이 남의 저장소다).
길이는 ggml 을 고치는 대신 생성 GGUF 의 텐서 이름을 줄여서 맞춘다(부모 저장소의
shared/compile/tensor_names.py, 근거는DECISIONS.md).제3자 클론으로 실측했다 — 클론 → 빌드 →
install_arch→ 재빌드 → 실행:남은 것
sparse_rcnn·queryinst— 배관 완료, 컴파일된 그래프 수치가 stage 0부터 갈린다(배관·가중치·fp16 은 무죄로 확인)
groie—GeneralizedAttention의 5D broadcast (렌더러)convnext·timm_example·reid— mmpretrain 의존 (격리 venv 필요)🤖 Generated with Claude Code