Skip to content

feat(gym): SVG→PNG GPU 래스터화 경로(vello/wgpu)와 정직한 벤치마크 - #4849

Closed
kevin9327 wants to merge 2 commits into
edwardkim:develfrom
kevin9327:gym_gpu_raster
Closed

feat(gym): SVG→PNG GPU 래스터화 경로(vello/wgpu)와 정직한 벤치마크#4849
kevin9327 wants to merge 2 commits into
edwardkim:develfrom
kevin9327:gym_gpu_raster

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

관련 이슈: #4848

요약

문서 코퍼스를 대량으로 이미지화해 비전 모델(VLM)에 먹이는 에이전트 파이프라인에서 실질 병목인 SVG → 픽셀(rasterization) 단계를, 새 cargo feature gpu 뒤에서 GPU(vello/wgpu)로 옮기는 경로를 추가한다. 파싱·레이아웃은 분기 지배적이라 GPU 대상이 아니며, 이 PR은 그 경계를 넘지 않는다 — 기존 SVG 산출(render_page_svg_native)을 그대로 재사용한다.

핵심은 정직한 측정이다: 같은 벡터 입력을 GPU(vello)와 CPU(resvg/tiny-skia)에 동일하게 넣어 순수 래스터화 시간과 픽셀 차이를 잰다. 결론부터 — 래스터화만 보면 GPU가 1678배 빠르지만, 엔드투엔드로는 1.42.2배에 그친다. 그 이유는 아래에 숫자로 밝힌다.

설계

HWP/HWPX ──(파싱·레이아웃, CPU·가속불가)──▶ SVG 문자열 ──(usvg 파싱, 공통)──▶ usvg::Tree
                                                                              │
                          ┌────────────────────────────────────────────────────┤
                          ▼                                                     ▼
              vello_svg → vello::Scene                                 resvg (CPU 기준선)
              → wgpu 컴퓨트 래스터 → 텍스처 → 리드백 → RGBA                → tiny_skia Pixmap → RGBA
                          └──────────────────────► PNG 인코딩(공통) ◄──────────┘
  • 재사용: SVG 생성은 기존 render_page_svg_native를 그대로 호출한다. 레이아웃 재구현 없음.
  • 단일 파싱: usvg::Tree를 한 번만 파싱해 GPU·CPU 두 경로에 동일 입력으로 넣는다. resvg 0.45와 vello_svg 0.7이 같은 usvg 0.45를 공유하도록 버전을 맞춰(카고 단일 노드) 픽셀 비교가 성립한다 — 차이는 레이아웃이 아니라 두 래스터라이저의 안티에일리어싱 방식만 반영한다.
  • 헤드리스: 서피스 없이 기본 어댑터(HighPerformance)를 자동 선택한다. GpuContext는 배치 전체에서 한 번만 생성해 재사용하고, 생성 비용(일회성)은 별도로 보고한다.
  • 컨텍스트 관리: wgpu가 GPU 컨텍스트를 관리하고, pollster로 async(adapter/device/버퍼매핑)를 블로킹한다.

Feature 게이팅 (CI는 컴파일, GPU는 로컬 실행)

native-skia와 정확히 같은 방식으로 선택 게이팅한다.

  • cargo build (gpu 없음): 여전히 컴파일. export-png-gpu/gpu-info는 사용법 오류(exit 2)로 안내 — export-png의 native-skia 스텁과 동일 계약.
  • cargo build --features gpu: GPU 경로 활성. 새 코드는 #[cfg(all(not(target_arch = "wasm32"), feature = "gpu"))]로 wasm·기본 빌드 양쪽에서 제외된다.
  • CI의 cargo fmt --all --check, cargo clippy -- -D warnings(기본 feature), wasm clippy 모두 GPU 스택을 컴파일하지 않는다.

정직한 벤치마크 (RTX 5080 Laptop, Vulkan)

export-png-gpu --benchmark가 페이지마다 같은 usvg::Tree를 GPU·CPU 두 래스터라이저로 굽고, 시간(반복 최솟값)과 픽셀 차이를 낸다.

래스터화 단계만 (GPU가 실제로 가속하는 부분)

표본 배율 페이지 픽셀 GPU/page CPU/page 래스터 speedup
6p HWP (2010-01-06) 2.0x 1588×2246 19.97 ms 1255.37 ms 62.9x
23p HWPX (교육통합) 2.0x 1588×2246 116.95 ms 1911.69 ms 16.4x
6p HWP (2010-01-06) 4.0x 3176×4492 60.96 ms 4755.81 ms 78.0x

엔드투엔드 (파싱·인코딩 등 공통 단계 포함)

표본 배율 CPU 경로 GPU(초기화 제외) E2E speedup GPU(초기화 포함)
6p 2.0x 17.40 s 9.99 s 1.74x 10.72 s (1.62x, init 732ms)
23p 2.0x 122.23 s 80.95 s 1.51x 84.67 s (1.44x, init 3.72s)
6p 4.0x 51.57 s 23.40 s 2.20x 24.62 s (2.09x, init 1.23s)

왜 엔드투엔드는 1.4~2.2배뿐인가 (정직한 진단)

래스터화는 GPU가 16~78배 빠르지만, 파이프라인에는 가속되지 않는 공통 CPU 단계가 있고 그게 총시간을 지배한다:

공통 단계 (두 경로 동일) 6p@2x 23p@2x 6p@4x
usvg 파싱+텍스트 셰이핑 773 ms/p 2145 ms/p 884 ms/p
PNG 인코딩 860 ms/p 839 ms/p 2941 ms/p
레이아웃+SVG 생성 12 ms/p 419 ms/p

GPU가 래스터화를 병목에서 제거하면, 파이프라인은 usvg 텍스트 셰이핑과 PNG 인코딩(둘 다 CPU·공통)에 묶인다. 이 PR은 그 두 단계를 건드리지 않는다.

어디서 GPU가 이기고, 어디서 아닌가

  • 이긴다: 고해상도일수록 래스터화 비중이 커져 이득이 커진다(배율 2→4에서 래스터 63→78x, E2E 1.74→2.20x). 배치가 클수록 일회성 초기화(0.7~3.7s)가 페이지당으로 상각된다.
  • 아니다/미미하다: 저해상도 단일 페이지 한 장(초기화 비용이 지배). 그리고 공통 파싱·인코딩 단계는 이 경로가 손대지 않으므로 엔드투엔드 상한이 여기서 정해진다.
  • 과장하지 않는다 — 위 숫자는 전부 --benchmark가 낸 실측이다.

시각 검증 (GPU vs CPU, 같은 벡터 입력)

전 표본 치수 100% 일치. 픽셀 차이는 극히 작다:

표본 최악 평균 픽셀차 |Δ|≥16 픽셀 비율
6p@2x 0.32 / 255 1.06%
23p@2x 1.22 / 255 1.93%
6p@4x 0.21 / 255 0.83%

차이는 레이아웃이 아니라 두 래스터라이저의 안티에일리어싱 방식 차이다. GPU 산출 PNG를 눈으로도 확인했다 — 한글 본문·표·괘선·각주가 정확히 렌더된다.

변경 파일

  • Cargo.toml: gpu feature + optional deps(vello 0.5, vello_svg 0.7, pollster 0.4, resvg-gpu=resvg 0.45).
  • Cargo.lock: 위 의존성 잠금.
  • src/renderer/gpu.rs(신규): GpuContext(vello/wgpu 재사용 컨텍스트), parse_svg(공통 트리), cpu_rasterize(resvg 기준선), rasterize(vello), diff(픽셀 비교), probe_adapters.
  • src/renderer/mod.rs: 게이팅된 pub mod gpu; 한 줄(rustfmt 미적용 — 규약).
  • src/main.rs: export-png-gpu·gpu-info 디스패치·구현(feature 유/무 두 변형)·capabilities 등록·help.

재현

cargo build --features gpu --bin rhwp
./rhwp gpu-info
./rhwp export-png-gpu "samples/2010-01-06.hwp" -o out --benchmark --repeat 5 --scale 2.0

한계 / 다음 단계

  • 엔드투엔드 상한은 공통 usvg 셰이핑·PNG 인코딩에 있다. 다음 이득은 그 두 단계(예: 셰이핑 결과 캐시, 빠른 PNG 압축, 페이지 병렬화)에서 난다.
  • vello가 텍스트를 직접 셰이핑하지 않으므로 현재는 usvg가 텍스트→패스 변환을 담당한다. 폰트는 시스템 폰트 + --font-path로 공급한다.
  • 초대형 배율에서 텍스처 한도(예: 16384px) 초과 시 wgpu가 오류를 반환한다 — 명확한 메시지로 전파된다.

파싱·레이아웃은 분기 지배적이라 GPU 대상이 아니다. 이 변경은 그 경계를
넘지 않고, 기존 SVG 산출(render_page_svg_native)이 만든 벡터를 픽셀로 굽는
래스터화 단계만 GPU로 옮긴다 — 대량 문서를 VLM 입력 이미지로 굽는 파이프라인용.

- 새 cargo feature `gpu` 뒤에 vello/vello_svg/wgpu 경로 추가(native-skia 와 동일 게이팅).
  CI 는 GPU 없이 컴파일, 실제 GPU 실행은 로컬.
- CLI `export-png-gpu`(+ `gpu-info`). feature 없이 빌드하면 사용법 오류(exit 2).
- `--benchmark`: 같은 usvg::Tree 를 GPU(vello)·CPU(resvg) 두 래스터라이저에 동일
  입력으로 넣어 순수 래스터화 시간과 픽셀 차이를 실측.

정직한 실측(RTX 5080 Laptop, Vulkan): 래스터화만 보면 GPU 16~78x,
엔드투엔드는 1.4~2.2x(상한은 공통 usvg 셰이핑·PNG 인코딩에 있음). 치수 100%
일치, 평균 픽셀차 <1.3/255. 어디서 이기고 어디서 아닌지 PR 본문에 숫자로 명시.

관련 이슈: edwardkim#4848

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jangster77
jangster77 self-requested a review August 15, 2026 13:29
CI 테스트 샤드가 실패했다 (PR edwardkim#4849):
agent_codex_contract::every_capability_command_has_a_codex_chapter —
"대전에 장이 없는 명령: [export-png-gpu, gpu-info]".

이 가드는 자기서술(capabilities)의 전 명령이 대전에 장을 갖도록 강제한다
— 명령만 늘리고 교본을 두면 문서가 CLI 를 못 따라간다. 두 명령 모두
category=export 라 변환·렌더 가족(40)에 등록하고 재생성했다.

검증: gen_agent_codex.py --check 변경 0(멱등), 재생성 산출에 절대 경로
0건·미분류 장 없음, agent_codex_contract 2/2, provenance_contract 10/10,
capabilities_subcommands_contract 4/4, cli_json_contract 31/31, rustfmt 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jangster77 pushed a commit that referenced this pull request Aug 15, 2026
CI 테스트 샤드가 실패했다 (PR #4849):
agent_codex_contract::every_capability_command_has_a_codex_chapter —
"대전에 장이 없는 명령: [export-png-gpu, gpu-info]".

이 가드는 자기서술(capabilities)의 전 명령이 대전에 장을 갖도록 강제한다
— 명령만 늘리고 교본을 두면 문서가 CLI 를 못 따라간다. 두 명령 모두
category=export 라 변환·렌더 가족(40)에 등록하고 재생성했다.

검증: gen_agent_codex.py --check 변경 0(멱등), 재생성 산출에 절대 경로
0건·미분류 장 없음, agent_codex_contract 2/2, provenance_contract 10/10,
capabilities_subcommands_contract 4/4, cli_json_contract 31/31, rustfmt 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jangster77

Copy link
Copy Markdown
Collaborator

통합 PR #4883(4412546)로 병합 완료했습니다.

원 head와 CI를 다시 확인해 누적 반영했고, 상세 검토·메인터너 보정·검증 근거는 archive 검토 기록에 남겼습니다.

중복 병합을 막기 위해 이 원 PR을 닫습니다. 감사합니다.

@jangster77 jangster77 closed this Aug 15, 2026
@jangster77

Copy link
Copy Markdown
Collaborator

통합 PR #4883(4412546)로 병합 완료했습니다.

원 head와 CI를 다시 확인해 누적 반영했고, 상세 검토·메인터너 보정·검증 근거는 archive 검토 기록에 남겼습니다.

중복 병합을 막기 위해 이 원 PR을 닫습니다. 감사합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants