임의 해상도 비전(Any-Resolution Vision) — Patch-n'-Pack과 NaFlex
현실의 이미지는 224x224 정사각형이 아닙니다. 영수증은 9:16, 차트(chart)는 16:9, 의료 스캔(medical scan)은 4096x4096, 모바일 스크린샷(mobile screenshot)은 9:19.5처럼 모양이 제각각입니다. 2024년 이전 시각-언어 모델(Vision-Language Model; VLM)의 답은 모든 입력을 고정 정사각형으로 리사이즈(resize)하는 것이었지만, 이 방식은 광학 문자 인식(Optical Character Recognition; OCR), 문서 이해(document understanding), 고해상도 장면 분석(high-resolution scene parsing)을 가능하게 만드는 신호(signal)를 버려 버립니다. NaViT(Google, 2023)는 가변 해상도(variable-resolution) 패치(patch)를 블록 대각(block-diagonal) 마스킹으로 한 트랜스포머(transformer) 배치(batch) 안에 묶어 넣을 수 있음을 보여주었습니다. Qwen2-VL의 M-RoPE(2024)는 절대 위치 테이블(absolute positional table)을 아예 제거했습니다. LLaVA-NeXT의 AnyRes는 고해상도 이미지를 베이스(base) 이미지와 서브 이미지(sub-image)로 타일링(tiling)했습니다. SigLIP 2의 NaFlex 변형(variant, 2025)은 단일 체크포인트(checkpoint) 하나로 모든 종횡비(aspect ratio)를 처리하려는 오픈 시각-언어 모델의 기본 인코더(encoder)로 자리잡았습니다. 이 강의에서는 Patch-n'-Pack을 처음부터 끝까지 구현합니다.
유형: Build
언어: Python (표준 라이브러리, 패치 패커(patch packer)와 블록 대각 마스크)
선수 지식: Phase 12 · 01 (ViT patches), Phase 12 · 05 (LLaVA)
예상 시간: 약 120분
학습 목표
- 가변 해상도 이미지들로 이루어진 배치의 패치를 하나의 시퀀스로 묶어 패킹(pack)하고, 블록 대각 어텐션 마스크(block-diagonal attention mask)를 만듭니다.
- 주어진 작업(task)에 대해 AnyRes 타일링(LLaVA-NeXT), NaFlex(SigLIP 2), M-RoPE(Qwen2-VL) 중 어떤 전략을 선택할지 판단합니다.
- OCR, 차트, 사진(photography) 작업에서 리사이즈 없이 토큰 예산(token budget)을 계산합니다.
- 정사각형 리사이즈가 갖는 세 가지 실패 양상(failure mode), 곧 텍스트가 찌그러지는 현상(squished text), 잘려나간 콘텐츠(cropped content), 패딩(padding)에 낭비되는 토큰을 설명합니다.
문제
트랜스포머는 시퀀스를 기대합니다. 배치는 길이가 같은 시퀀스를 쌓아 놓은 것입니다. 이미지가 모두 224x224라면 매번 196개의 패치 토큰(patch token)이 나오고, 패딩도 필요 없으며, 그것으로 끝입니다. 224에서 학습하고 224에서 추론한다면 해상도(resolution)는 더 이상 고민거리가 아닙니다.
그러나 현실은 그렇게 협조적이지 않습니다. 문서는 세로 방향(portrait)입니다. 예를 들어 8.5x11 인치 문서는 대략 2:3 비율을 가집니다. 차트 스크린샷은 가로 방향(landscape) 16:9입니다. 영수증은 길고 좁아서 1:3에 가깝습니다. 의료 영상(medical imaging)은 2048x2048 이상으로 제공되기도 합니다. 모바일 기기 스크린샷은 1170x2532, 즉 0.46:1 비율을 가집니다.
2024년 이전에는 세 가지 선택지가 있었고, 각각 분명한 실패 이유를 가지고 있었습니다.
- 고정 정사각형(224x224 또는 336x336)으로 리사이즈합니다. 가로세로를 강제로 맞추다 보면 텍스트와 얼굴이 왜곡되며, 다운스케일(downscale) 과정에서 차트 라벨과 OCR 대상 콘텐츠가 손상됩니다. LLaVA-1.5 이전까지는 이 방식이 표준이었습니다.
- 고정 종횡비로 잘라냅니다(crop). 이미지의 대부분을 버리게 되고, 어디를 자를지 정하는 것 자체가 또 하나의 비전 문제가 됩니다.
- 가장 긴 변에 맞춰 패딩합니다. 왜곡은 해결하지만 세로 방향 이미지에서는 토큰의 50% 이상을 패딩이 차지하며, 그 패딩 토큰에도 이차(quadratic) 어텐션 비용이 계속 부과됩니다.
2024-2025년의 답은 트랜스포머가 이미지의 원본 해상도(native resolution)에서 곧바로 패치를 받아들이게 하고, 서로 다른 모양의 이미지들로 이루어진 이질적(heterogeneous) 배치를 낭비 없이 하나의 시퀀스로 묶는 방법을 마련하는 것이었습니다.
개념
NaViT와 Patch-n'-Pack
NaViT(Dehghani et al., 2023)는 이 방식이 대규모 환경(scale)에서도 동작함을 처음 보여 준 논문입니다. 아이디어 자체는 기계적이며 단순합니다.
- 배치의 각 이미지에 대해 선택한 패치 크기(patch size), 예를 들어 14에서 원본 패치 격자(native patch grid)를 계산합니다.
- 각 이미지의 패치를 자기만의 가변 길이 시퀀스로 펼칩니다(flatten).
- 모든 이미지의 패치를 하나의 긴 시퀀스로 이어 붙입니다(concatenate).
- 이미지 A의 패치가 이미지 A 내부에서만 어텐션을 수행하도록 블록 대각 어텐션 마스크를 만듭니다.
- 각 패치의 위치 정보(2D 회전 위치 임베딩(2D RoPE) 또는 분수 위치 임베딩(fractional position embedding))를 함께 운반합니다.
336x336(576 토큰), 224x224(256 토큰), 448x336(768 토큰) 이미지 세 장으로 이루어진 배치는, 길이 1600인 단일 시퀀스와 1600x1600 블록 대각 마스크 하나로 표현됩니다. 패딩도 없고 낭비되는 연산도 없으며, 트랜스포머는 임의의 종횡비를 그대로 처리할 수 있습니다.
NaViT는 학습 중 분수 패치 드롭(fractional patch dropping)도 함께 도입했습니다. 배치 전체에서 패치의 50%를 무작위로 떨어뜨리는 방식인데, 이는 정규화(regularization)와 학습 가속을 동시에 제공합니다. SigLIP 2도 이 아이디어를 이어받았습니다.
AnyRes (LLaVA-NeXT)
LLaVA-NeXT의 AnyRes는 보다 현실적인 대안입니다. 고해상도 이미지와 고정된 인코더(예: 336 해상도의 CLIP 또는 SigLIP)가 주어졌을 때, 이미지를 타일링합니다.
- 미리 정의해 둔 격자 레이아웃(grid layout) 후보 중 이미지의 종횡비에 가장 잘 맞는 것을 고릅니다. 예를 들어 (1x1), (1x2), (2x1), (1x3), (3x1), (2x2) 같은 후보가 있습니다.
- 전체 이미지를 격자에 맞춰 타일링합니다. 각 타일(tile)은 336x336 크롭이 됩니다.
- 함께 사용할 썸네일(thumbnail)도 하나 만듭니다. 이는 전체 이미지를 336x336으로 리사이즈한 전역 문맥 토큰(global-context token)입니다.
- 모든 타일을 동결된(frozen) 336 인코더에 통과시킨 뒤, 타일 토큰과 썸네일 토큰을 이어 붙입니다.
672x672 이미지를 2x2 격자와 썸네일로 처리하면 4 * 576 + 576 = 2880개의 시각 토큰(visual token)이 만들어집니다. 비용이 크지만 효과적입니다. 대형 언어 모델(Large Language Model; LLM)이 국소적 디테일(local detail)과 전역 맥락(global context)을 동시에 볼 수 있기 때문입니다.
AnyRes는 인코더가 동결되어 있고 단일 해상도만 지원할 때 적합한 경로입니다. 다만 큰 이미지에서는 토큰 개수가 폭발합니다. 1344x1344 이미지를 4x4 격자로 처리하면 9216 + 576 ≈ 9800 토큰이 되어, 8천(8k) 토큰짜리 LLM 컨텍스트(context)의 대부분을 이미지 한 장이 차지해 버립니다.
M-RoPE (Qwen2-VL)
Qwen2-VL은 다중모드 회전 위치 임베딩(Multimodal Rotary Position Embedding; M-RoPE)을 도입했습니다. NaViT의 분수 위치나 AnyRes의 타일-썸네일 방식 대신, 각 패치가 3차원 위치를 가집니다. 세 축은 시간(temporal), 높이(height), 너비(width)입니다. 쿼리(query)와 키(key) 회전이 임의의 H, W, 시간 길이를 각자에게 맞는 주파수(frequency)로 처리합니다.
M-RoPE는 재학습 없이 원본 동적 해상도(native dynamic resolution)를 제공합니다. 추론(inference) 시 임의의 HxW 이미지를 넣으면 패치 임베더(patch embedder)가 H/14 x W/14 토큰을 만들고, 각 토큰은 (t=0, r=row, c=col) 위치를 받습니다. RoPE가 알맞은 주파수로 어텐션을 회전시키면 끝입니다. Qwen2.5-VL과 Qwen3-VL도 이 흐름을 그대로 이어 갑니다. InternVL3의 V2PE도 모달리티(modality)별로 다른 인코딩을 적용한다는 점에서 같은 발상에 해당합니다.
AnyRes와 달리 M-RoPE는 원본 해상도에서 O(H x W / P^2) 토큰만 사용합니다. 타일 오버헤드가 곱으로 늘어나지 않습니다. NaViT와 달리 한 번의 순방향(forward) 연산에서는 여전히 이미지 한 장만을 기대합니다. 여러 해상도를 한 배치로 묶으려면 그 위에 Patch-n'-Pack을 한 겹 더 얹어야 합니다.
NaFlex (SigLIP 2)
NaFlex는 SigLIP 2 체크포인트의 원본-가변(native-flex) 모드입니다. 하나의 모델이 추론 시 여러 시퀀스 길이, 예를 들어 256, 729, 1024 토큰을 처리합니다. 내부적으로는 학습 중 NaViT 방식의 Patch-n'-Pack과 패치별 절대 분수 위치(absolute fractional position)를 사용합니다. 핵심 장점은 단일 체크포인트로 작업에 맞는 토큰 예산을 추론 시점에 골라 쓸 수 있다는 것입니다.
의미 기반 작업(semantic task; 분류(classification), 검색(retrieval))에는 256 토큰을 씁니다. OCR이나 차트 이해에는 1024 토큰을 씁니다. 재학습은 필요하지 않습니다.
패킹 마스크(Packing mask)
블록 대각 마스크는 구현이 가장 자주 어긋나는 지점입니다. 길이 N_total인 패킹된 시퀀스가 i=0..B-1 이미지들을 담고 있고 각 이미지의 길이가 n_i라고 합시다. 형태가 (N_total, N_total)인 마스크 M은 두 인덱스가 같은 이미지 블록 안에 들어 있으면 1이고, 아니면 0입니다. 누적 길이(cumulative length) 리스트로부터 다음과 같이 구성할 수 있습니다.
offsets = [0, n_0, n_0+n_1, ..., N_total]
M[i, j] = 1 iff there exists b where offsets[b] <= i < offsets[b+1] and offsets[b] <= j < offsets[b+1]
PyTorch에서는 torch.block_diag나 명시적 gather 연산으로 한 줄에 만들 수 있습니다. FlashAttention의 가변 길이 경로(variable-length path)인 cu_seqlens는 밀집 마스크(dense mask)를 아예 만들지 않습니다. 대신 누적 길이 텐서를 받아 각 시퀀스 내부에서만 어텐션을 수행합니다. 일반적인 배치에서는 밀집 마스크 방식보다 대략 10배 빠릅니다.
토큰 예산(Token budget)
작업 성격에 따라 전략을 고릅니다.
- OCR과 문서: 1024-4096 토큰입니다. SigLIP 2 NaFlex @ 1024 또는 AnyRes 3x3 + 썸네일이 좋습니다.
- 차트와 사용자 인터페이스(UI): 원본 해상도 384-448에서 729-1024 토큰 수준입니다.
max_pixels 상한이 있는 Qwen2.5-VL 동적 해상도가 잘 맞습니다.
- 자연 사진(natural photo): 256-576 토큰이면 충분합니다. 뒤따르는 LLM은 이 정도면 충분한 정보를 받습니다. 콘텐츠 밀도가 높은 곳에만 토큰 비용을 집중시키는 셈입니다.
- 비디오(video): 공간 풀링(spatial pooling) 이후 프레임당 64-128 토큰, 초당 2-8 프레임(FPS) 정도를 사용합니다. Lesson 12.17에서 다룹니다.
2026년 운영 환경(production)의 일반 규칙은 다음과 같습니다. 작업별로 max-pixels 상한을 정하고, 그 상한 안에서 원본 종횡비 그대로 인코딩하며, 배치를 패킹하고, 패딩은 건너뜁니다. Qwen2.5-VL은 이 노브(knob)를 위해 min_pixels와 max_pixels를 직접 노출합니다.
사용해보기
code/main.py는 정수 픽셀 좌표(integer pixel coordinate)를 사용하는 이질적 이미지 배치에 대해 Patch-n'-Pack을 구현합니다. 주요 동작은 다음과 같습니다.
(H, W) 형태의 이미지 크기 목록을 받습니다.
- 패치 크기 14에서 각 이미지의 패치 시퀀스 길이를 계산합니다.
sum(n_i) 길이의 단일 시퀀스로 패킹합니다.
- 블록 대각 어텐션 마스크를 만듭니다. 이해를 돕기 위해 밀집 마스크 형태로 구현합니다.
- 패킹 비용을 정사각형 리사이즈, AnyRes 타일링 비용과 비교합니다.
- 영수증, 차트, 스크린샷, 사진이 섞인 배치에 대한 토큰 예산 표를 출력합니다.
직접 실행해 보세요. 출력되는 숫자가, 2026년 대부분의 오픈 시각-언어 모델이 Patch-n'-Pack을 채택하는 이유 그 자체입니다.
산출물 만들기
이 강의는 outputs/skill-resolution-budget-planner.md를 산출합니다. 혼합 종횡비 워크로드(OCR, 차트, 사진, 비디오 프레임)와 총 토큰 예산이 주어지면, 적절한 전략(NaFlex, AnyRes, M-RoPE, 정사각형 리사이즈)을 골라 요청별 구성을 출력합니다. 제품용 시각-언어 모델을 산정할 때 이 스킬을 사용하면, 지연 예산(latency budget)을 무너뜨리는 조용한 10배 토큰 폭증(token blowup)을 미리 막을 수 있습니다.
연습문제
-
쉬움: 영수증의 크기가 600x1500(1:2.5)입니다. 패치 크기 14에서 원본 해상도의 토큰은 몇 개입니까? 336 정사각형으로 리사이즈한 뒤에는 몇 개입니까? 실제 OCR 정확도에서는 어느 쪽 손실이 더 큽니까?
-
중간: 길이가 256, 576, 729, 1024인 이미지 네 장으로 이루어진 배치에 대해 블록 대각 마스크를 만듭니다. 어텐션 행렬이 2585x2585이고, 0이 아닌(non-zero) 원소 수가 정확히 256^2 + 576^2 + 729^2 + 1024^2인지 확인합니다.
-
중간: 1792x896 이미지, 패치 크기 14에서 다음 세 방식을 비교합니다. (a) 336으로 정사각형 리사이즈 후 인코딩, (b) AnyRes 2x1 + 썸네일, (c) 원본 해상도의 M-RoPE. 어떤 방식이 토큰을 가장 적게 사용합니까? 어떤 방식이 디테일을 가장 잘 보존합니까?
-
어려움: 분수 패치 드롭을 구현합니다. 패킹된 시퀀스가 주어졌을 때 토큰의 50%를 균등 무작위(uniform random)로 떨어뜨리고, 블록 대각 마스크를 그에 맞춰 갱신합니다. 마스크 희소성(sparsity)의 변화도 측정합니다.
-
어려움: Qwen2-VL 논문 Section 3.2(arXiv:2409.12191)를 읽습니다. min_pixels와 max_pixels가 무엇을 제어하는지, 그리고 왜 두 경계(bound)가 모두 중요한지 두 문장으로 설명합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| Patch-n'-Pack | "NaViT 방식 패킹" | 서로 다른 이미지에서 나온 가변 길이 패치 시퀀스를 하나의 배치 차원으로 이어 붙이는 방식이다. |
| 블록 대각 마스크(Block-diagonal mask) | "패킹 마스크" | 각 이미지의 패치가 패킹된 시퀀스 안의 이웃 이미지가 아니라 자기 자신에게만 어텐션하도록 제한하는 어텐션 마스크이다. |
| AnyRes | "LLaVA-NeXT 타일링" | 고해상도 이미지를 고정 크기 타일 격자와 전역 썸네일로 나눈 뒤, 모든 타일을 고정 인코더로 인코딩하는 방식이다. |
| NaFlex | "SigLIP 2 원본-가변(native-flex)" | 재학습 없이 추론에서 256/729/1024 토큰 예산을 모두 처리하는 단일 SigLIP 2 체크포인트이다. |
| M-RoPE | "다중모드 RoPE(Multimodal RoPE)" | 위치 테이블 없이 임의의 H, W, T를 처리하는 3D 회전 위치 인코딩(time, row, column)이다. |
cu_seqlens | "FlashAttention 패킹 텐서" | FlashAttention의 가변 길이 경로가 밀집 블록 대각 마스크 대신 사용하는 누적 길이 텐서이다. |
min_pixels / max_pixels | "해상도 경계(Resolution bounds)" | 매우 작거나 큰 입력에 대한 토큰 수를 제한하는 Qwen2.5-VL의 요청 단위 노브이다. |
| 시각 토큰 예산(Visual token budget) | "이미지당 토큰 수" | 이미지 한 장에서 나오는 패치 토큰의 대략적 개수이며, LLM 프롬프트 예산과 어텐션 비용을 결정한다. |
더 읽을거리