본문 바로가기

Dev Log

구글의 터보퀀트, 그리고 압축이란 무엇인가

728x90

기존 압축의 딜레마

압축을 하면 부가 정보가 생긴다. 압축된 데이터로 연산하려면 "이 데이터가 어떤 범위에서 압축됐는지" 기준값을 따로 저장해야 하는데, 이걸 압축 상수 또는 메타데이터라고 한다.

 

기존 기법들은 KV 캐시 비트 수를 줄이려다 정확도를 함께 잃거나, 이 메타데이터 오버헤드가 늘어나는 문제가 있었다. 압축은 했는데 메타데이터가 따라붙으니 실질 효과가 반감되는 것.

원본 벡터 저장: 4비트
압축 상수 저장: +1~2비트
실질 압축 효과: 생각보다 별로

터보퀀트가 풀려는 문제가 바로 이거다. 이 오버헤드를 0으로 만들자.

 

터보퀀트가 하는 일

1단계 — 좌표계를 바꾼다 (PolarQuant)

보통 위치를 표현할 때 X, Y, Z를 쓴다. "동쪽으로 3블록, 북쪽으로 4블록" 이런 식으로. 이걸 극좌표로 바꾸면 "37도 방향으로 5블록"이 된다. 같은 위치인데 표현 방식만 다른 것.

직교좌표: "동쪽으로 3블록, 북쪽으로 4블록"
극좌표:   "37도 방향으로 5블록"

 

PolarQuant는 AI 벡터를 이 극좌표 방식으로 변환한다. 그러면 반지름(크기)각도(방향) 두 가지로 분리된다.

여기서 재밌는 점이 있다. 극좌표로 변환하면 각도의 분포가 예측 가능한 원형 패턴을 따르게 된다. 직교좌표는 경계가 매번 달라지는 정사각형 격자라서 "이 데이터의 범위가 어디서 어디까지야"를 매번 계산해야 한다. 근데 극좌표는 경계가 고정된 원이라 그 계산 자체가 필요 없어진다. 메타데이터를 따로 저장 안 해도 되니, 오버헤드가 사라지는 거다.

 

2단계 — 미세한 오차를 잡는다 (QJL)

1단계 압축을 하면 아무리 잘해도 아주 미세한 오차가 생긴다. QJL은 존슨-린덴스트라우스 변환이라는 수학 기법을 이용해서, 이 오차가 한쪽으로 치우치지 않도록 통계적으로 상쇄한다. 각 숫자를 +1 또는 -1 부호 하나로만 표현하고, 오버헤드는 사실상 0.

 

결과

압축 전: float16 → 16비트
압축 후: 3비트
───────────────────────
메모리:      1/6로 감소
속도:        H100 기준 최대 8배 향상
정확도 손실: 없음
파인튜닝:    불필요

기존 모델을 건드리지 않고 그냥 얹을 수 있다. 재학습도, 파인튜닝도 없다. 추론 단계에서만 작동한다.

 

더 자세한 내용은: 

https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/

 

TurboQuant: Redefining AI efficiency with extreme compression

Vectors are the fundamental way AI models understand and process information. Small vectors describe simple attributes, such as a point in a graph, while “high-dimensional” vectors capture complex information such as the features of an image, the meani

research.google

 

 

압축??? 생각해보니 나 압축에 대해 아는 게 없었다

터보퀀트를 파고들다 보니 자연스럽게 이런 생각이 들었다. 압축이 정확히 뭐지?ㅇ_ㅇ? 그래서 압축이라는 개념 자체를 한번 정리해봤다.

압축 알고리즘의 계보

결국 한문장으로 표현하자면 : 압축 = 동일한 정보를 더 적은 비트로 표현한다

 

압축은 크게 두 계열로 나뉜다.

무손실 계열 — 정보를 하나도 안 버림.

허프만 코딩   → 자주 나오는 건 짧게, 드문 건 길게
              (ZIP, PNG 기반)

LZ계열        → 반복 패턴을 참조로 대체
(LZ77, LZ78)   (ZIP, gzip, 대부분의 압축 포맷 기반)

Zstandard     → LZ계열 발전형, 현재 가장 빠른 무손실 압축
              (Meta가 만들어서 오픈소스 공개)

 

손실 계열 — 사람이 못 느끼는 부분을 버림

DCT 기반      → 주파수 분해 후 고주파 제거
              (JPEG, MP3, 동영상 코덱)

웨이블릿 기반  → DCT보다 정교한 주파수 분해
              (JPEG2000, 의료영상)

신경망 기반    → AI가 압축 방식 자체를 학습
              (최신 이미지/영상 압축 연구)

터보퀀트는 이 계보에서 벡터 양자화 계열에 속한다. 손실처럼 보이지만 AI가 실제로 쓰는 정보인 방향과 순서는 보존하기 때문에, 실질적으로는 무손실에 가까운 독특한 포지션이다.

결국 모든 압축의 공통 철학은 하나다.

"보는 사람, 또는 쓰는 시스템이 구분 못하는 차이는 버려도 된다."

 

사람 눈이 못 보는 고주파는 JPEG가 버리고, 반복 패턴은 ZIP이 줄이고, 변하지 않는 배경은 동영상 코덱이 건너뛰고, AI가 안 쓰는 벡터 크기 정보는 터보퀀트가 압축한다. 접근은 달라도 철학은 같다.

 

프론트엔드에서의 압축

그렇다면 프론트엔드에서 압축은 어떻게 적용되고 있을까?

프론트엔드에서 파일 크기를 줄이는 방법은 세 가지가 섞여 있는데, 원리가 다 다르다.

진짜 압축  → 같은 정보를 더 적은 비트로 인코딩 (Gzip, Brotli)
경량화     → 불필요한 표현 제거, 정보량은 동일 (Minification)
코드 제거  → 아예 안 쓰는 코드를 없앰 (Tree Shaking)

 

경량화 (Minification) — 불필요한 표현 제거

엄밀히 압축이 아니다. 정보를 줄이는 게 아니라 불필요한 표현을 제거하는 것. 의미는 같은데 표기를 줄이는 거다.

// 원본
function calculateTotal(price, quantity) {
  const total = price * quantity;
  return total;
}

// Minify 후
function c(p,q){return p*q}

변수명 price를 p로 바꿔도 코드가 하는 일은 똑같다. 사람이 읽기 편하라고 썼던 긴 이름을, 기계만 읽으면 되니까 짧게 줄인 것. 공백, 줄바꿈, 주석도 전부 제거한다. Webpack, Vite, esbuild가 빌드할 때 자동으로 해준다.

 

Tree Shaking — 안 쓰는 코드 제거

경량화보다 더 근본적이다. 표현을 줄이는 게 아니라 코드 자체를 없애버린다.

// lodash 전체를 import하면
import _ from 'lodash'             // 70KB 전부 포함

// 필요한 것만 가져오면
import { debounce } from 'lodash'  // 쓰는 것만 포함

번들러가 실제로 호출되는 코드만 남기고 나머지는 최종 파일에서 제거한다. 라이브러리가 ESM 형식이어야 제대로 동작하고, rollup-plugin-visualizer 같은 도구로 어떤 라이브러리가 얼마나 차지하는지 시각화해서 볼 수 있다.

 

진짜 압축 — Gzip / Brotli

Minify와 Tree Shaking이 끝난 파일을 서버가 브라우저로 전송할 때 한 번 더 압축한다. 여기서 비로소 앞에서 말한 LZ계열 알고리즘이 등장한다.

// 브라우저 요청 헤더
Accept-Encoding: gzip, deflate, br

// 서버 응답 헤더
Content-Encoding: br  ← Brotli로 압축해서 보냄

Gzip이 오래된 표준이고, Brotli는 구글이 만든 최신 버전이다. 같은 파일을 Brotli로 압축하면 Gzip보다 20~30% 더 작아진다. Nginx나 CDN 설정에서 켜고 끄는 그 옵션이 바로 이거다.

 

LZ계열이 하는 일 — 반복을 참조로 대체

핵심 아이디어는 단순하다. "이미 나온 내용이 또 나오면, 다시 쓰지 말고 '아까 거기 있던 거' 라고 가리키자."

 

LZ77 — 슬라이딩 윈도우

LZ77 — 슬라이딩 윈도우
원본 텍스트:
"abcabcabcabc"

처음 "abc" 등장 → 그냥 저장
두 번째 "abc"   → "3글자 앞으로 돌아가서 3글자 복사해"
세 번째 "abc"   → 똑같이
네 번째 "abc"   → 똑같이

압축 결과:
"abc" + (3,3) + (3,3) + (3,3)
(3,3)이 의미하는 건 "3칸 앞, 3글자 길이" 야. 실제 문자 대신 좌표를 저장하는 것.
원본:   a b c a b c a b c a b c   (12글자)
압축:   a b c (3,3) (3,3) (3,3)   (훨씬 짧음)

슬라이딩 윈도우란 과거 데이터를 담아두는 버퍼야. 새 데이터가 들어올 때마다 이 창문을 옆으로 밀면서 "최근 N글자 안에 이 패턴이 있었나?" 탐색한다.

[과거 버퍼 - 탐색 범위]  [앞으로 읽을 데이터]
 a b c a b c            a b c a b c
              ↑
         지금 여기서 "abc"를 발견
         → 6칸 앞에 있던 거랑 같다 → (6,3) 저장

 

 

LZ78 — 사전 방식

LZ77이 "몇 칸 앞"이라는 위치로 참조한다면, LZ78은 사전(dictionary) 을 만들어가면서 압축한다.

입력: a b a b c a b c

처음 "a"  → 사전에 없음 → 저장, 사전에 추가 {1: "a"}
"b"       → 사전에 없음 → 저장, 사전에 추가 {2: "b"}
"ab"      → 사전에 없음 → 저장, 사전에 추가 {3: "ab"}
"abc"     → 사전에 없음 → 저장, 사전에 추가 {4: "abc"}
"ab"      → 사전에 있음! → 번호 3으로 대체
"c"       → 사전에 있음  → 번호로 대체

 

리액트 컴포넌트를 예로 들어보자.

실제 흐름

내가 짠 코드
→ 번들러 (Vite/Webpack)가 하나로 합침
→ Minification (경량화)
→ 서버에 올라감
→ 브라우저가 요청
→ 서버가 Gzip/Brotli로 압축해서 전송  ← 여기서 LZ 등장
→ 브라우저가 받아서 압축 해제 후 실행

실제로 component가 압축되는 과정

// Button.jsx
function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  )
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  )
}

1단계 — 번들러가 하나로 합침

// 여러 파일이 main.js 하나로
function Button({onClick,children}){
  return React.createElement("button",{onClick},children)
}
function Card({onClick,children}){
  return React.createElement("div",{onClick},children)
}
// ... 수백 개 컴포넌트

2단계 — Minification

// 변수명 압축, 공백 제거
function B({o,c}){return R.c("button",{onClick:o},c)}
function C({o,c}){return R.c("div",{onClick:o},c)}

3단계 — 브라우저가 요청하면 그때 LZ로 압축 전송

LZ77이 보는 것:

"function B({o,c}){return R.c("button",{onClick:o},c)}"
"function C({o,c}){return R.c("div",{onClick:o},c)}"

→ "function " 반복 발견       → (참조)
→ "({o,c}){return R.c(" 반복  → (참조)
→ ",{onClick:o},c)}" 반복     → (참조)

압축 결과:
"function B({o,c}){return R.c("button",{onClick:o},c)}"
(22,8)(22,18)"div"(22,14)

 

다시 돌아가서 결론을 얘기하자면 : 경량화, tree shaking, 압축이 세 단계가 합쳐지면 이렇게 된다.

원본 JS 파일: 500KB
→ Tree Shaking:  300KB  (안 쓰는 코드 제거)
→ Minification:  150KB  (불필요한 표현 제거)
→ Brotli 압축:    40KB  (진짜 압축)

같은 코드인데 브라우저가 받는 건 40KB. 열두 배 차이다.

 

느낀점

압축이란 개념을 파고들다 보니 결국 나온 건 슬라이딩 윈도우 알고리즘이었다.

처음엔 그냥 "파일 크기 줄이는 기술" 정도로만 알고 있었는데, 타고 타고 들어가다 보니 결국 알고리즘…

터보퀀트 → KV캐시 압축 → 벡터 양자화
→ 압축이란 뭔가 → LZ계열 → 슬라이딩 윈도우

이게 좀 신기했다. AI 반도체 기사 하나 읽다가 도달한 곳이 알고리즘 기초라니.

 

터보퀀트를 읽으면서도 비슷한 걸 느꼈다. 구글이 완전히 새로운 걸 발명한 게 아니었다. 극좌표 변환이라는 수학, 존슨-린덴스트라우스 변환이라는 기존 이론을 가져다가 조합한 거였다. LZ77도 마찬가지다. 1977년에 나온 알고리즘이 지금도 gzip과 Brotli 안에서 그대로 돌아가고 있다. 수십 년이 지나도 바뀌지 않는 것들이 있다.

 

결국 중요한 건 어떤 문제를 풀 것인가다. 그 문제가 정해지면 나머지는 다 수단이고 방법이다. 그리고 그 수단과 방법의 core에는 항상 기초 개념들이 있다. 기술 스택은 계속 바뀌지만 그 아래 깔린 원리는 잘 안 바뀐다. 최신 AI 압축 기술도, 수십 년 된 파일 압축도, 결국 해답은 기본 개념에서 출발해서 조금 더 발전하고 적용된 것들이었다.

 

왜 CS 기본기를 강조하는지 새삼 다시 느꼈다. (알고리즘 공부를 더 열심히 해야하나 껄껄)

728x90