본문으로 건너뛰기

© 2026 Molayo

r/LocalLLaMA분석2026. 06. 28. 00:33

Rust로 Qwen2.5 추론 엔진을 밑바닥부터 작성했습니다. 흥미로운 점은 Transformer가 아니라 순전파(Forward Pass)를

요약

Rust를 사용하여 외부 라이브러리 의존성을 최소화한 Qwen2.5 추론 엔진 'ignis'를 개발했습니다. SSA 중간 표현(IR)을 활용한 연산 퓨전과 메모리 계획 최적화 패스를 통해 효율적인 추론을 구현했습니다.

핵심 포인트

  • PyTorch나 candle 없이 순수 Rust로 구현된 엔드 투 엔드 추론 엔진
  • SSA IR 기반의 연산 퓨전으로 연산 수 감소 및 중간 버퍼 제거
  • 레지스터 할당 방식의 메모리 계획을 통해 활성화 버퍼 사용량 76% 절감
  • Apple M3 환경에서 NEON 커널을 활용한 성능 최적화 수행

이 프로젝트의 이름은 ignis입니다. GGUF 파일에서 qwen2.5-0.5b를 로드하며, 두 개의 의존성(mmap된 가중치 블롭을 위한 memmap2, 토크나이저(Tokenizer)의 한 가지 예외 케이스를 위한 fancy-regex)만을 사용하여 순수 Rust로 엔드 투 엔드(end-to-end) 실행됩니다. PyTorch, ggml, candle 또는 blas를 사용하지 않았습니다. 모든 레이어(layer)를 직접 작성했습니다.

사람들이 보고 싶어 하는 것이 성능이라는 것을 알기에 성능을 먼저 보여드립니다:

포맷 크기 디코딩 tok/s

q8_0 640 MB -52
q4_0 410 MB -26
f16 1.2 GB -8

Apple M3, 탐욕적 디코딩(greedy decode) 기준입니다. llama.cpp에 근접하지 않으며, 근접한 척하지도 않습니다. 핫 패스(hot paths)에는 직접 작성한 NEON 커널(Q8_0 int8→int32→f32 widening + FMA, f32 reducer)을 사용하지만, llama.cpp와의 실제 격차를 줄이려면 정수 도메인 양자화 점곱(integer-domain quantized dot products)과 제가 구축하지 않은 지속적인 스레드 풀(thread pool)이 필요합니다. 정확성과 컴파일러가 우선이었습니다.

제가 실제로 가장 신경 썼던 부분
대부분의 밑바닥부터 만든 추론(inference) 프로젝트들은 순전파(forward pass)를 고정된 함수 호출 시퀀스로 실행합니다. ignis는 그렇게 하지 않습니다. 대신 순전파를 SSA 중간 표현(Intermediate Representation)으로 낮추고(lowering), 실행하기 전에 두 번의 컴파일러 패스(compiler passes)를 수행합니다.

패스 1, 퓨전(fusion): 낮춰진 그래프는 RMSNorm을 (normalize + scale-by-gain)으로, SwiGLU를 (SiLU + elementwise multiply)로 방출합니다. 퓨전 패스는 이러한 쌍을 인식하여 단일 커널로 병합하며, 이를 통해 활성화(activation)에 대한 전체 패스와 매칭당 하나의 중간 버퍼를 제거합니다. qwen2.5-0.5b 기준: 435개 연산(ops)에서 362개 연산으로 감소했습니다 (49개의 RMSNorm 퓨전, 24개의 SwiGLU 퓨전).

패스 2, 메모리 계획(memory planning): 변수 대신 활성화 텐서(activation tensors)에 레지스터 할당(register allocation)을 적용합니다. 수명이 겹치지 않는 값들은 물리적 버퍼를 공유합니다. 연산 스케줄에 대한 단일 선형 스캔(linear scan)으로 이를 처리합니다. 단순한 디코딩 단계에서는 363개의 활성화 버퍼(~2.7 MB)가 필요했습니다. 계획 적용 후에는 5개의 재사용된 버퍼(~0.67 MB)만 필요했습니다. 출력의 변경 없이 76%를 절감했습니다.

정확성은 컴파일된 그래프 엔진과 원래의 명령형 참조(imperative reference)를 동일한 프롬프트에서 실행하여, 바이트 단위로 동일한 토큰 스트림과 일치하는 로짓(logits)을 요구하는 패리티 테스트(parity test)를 통해 고정됩니다.

최적화 패스(optimization passes)가 신뢰할 수 있는 이유는 오직 해당 테스트가 존재하기 때문입니다.
CLI는 실행할 때마다 컴파일러가 실제로 수행한 작업을 보여줍니다:

$ ignis run -m models/qwen2.5-0.5b-instruct-q8_0.gguf
-p "In two sentences, what makes a compiler different from an interpreter?"
qwen2 로드됨 (24 layers, vocab 151936) 0.07s 소요
compiler: 435 ops -> 362 ops (49 rmsnorm + 24 swiglu fused)
compiler: 363 activation values -> 5 reused buffers (2760.5 KB -> 669.5 KB activations, 76% 절약)
컴파일러는 소스 코드를 기계어로 번역하는 프로그램인 반면,
인터프리터(interpreter)는 코드를 직접 실행하는 프로그램입니다.
[32 prompt tok in 0.55s (58.1 tok/s) | 24 gen tok in 0.46s (52.1 tok/s)]

repo: https://github.com/arya51-ai/ignis
구현 세부 사항, 특히 컴파일러 설계에 대해 논의하게 되어 기쁩니다. 개인 프로젝트를 위해 메모리 계획 패스(memory planning pass)를 구현해 본 다른 분이 계신지, 그리고 제 구현에서 성능(perf) 손실이 발생할 만한 부분이 어디일지 진심으로 궁금합니다.
제출자: /u/aryamehta
[link] [comments]

AI 자동 생성 콘텐츠

본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0