토크나이저 v1, 인코딩과 디코딩과 확장을 직접 재다

Hugging Face의 tokenizers v1 측정 글을 따라 인코딩과 디코딩의 비대칭, 왕복 정확도, 길이와 배치와 동시성에 따른 병목을 살펴봅니다.

원문 논문 보기
'토크나이저 v1, 인코딩과 디코딩과 확장을 직접 재다' 기사 커버 이미지

Hugging Face가 블로그 글 tokenizers v1: encode, decode and scaling, measured를 공개했습니다. 이 글은 토크나이저의 인코딩과 디코딩을 따로 떼어 측정하고 길이와 배치와 동시성에 따라 어떻게 달라지는지를 보여주겠다는 주장을 한 문장으로 말하자면 토크나이저를 감이 아니라 측정으로 다루기 위한 v1 기준점을 세우겠다는 것입니다. 원문은 제목 아래에 연결되어 있습니다.

안녕하세요, 패트릭입니다. 저는 이 글을 처음 봤을 때 솔직히 잠깐 망설였습니다. 토크나이저 이야기는 모델 구조 이야기에 비해 눈에 잘 띄지 않으니까요. 그런데 곰곰이 생각해보면 제가 운영 상담을 할 때마다 결국 돌아오는 질문은 토큰 쪽이었습니다. 왜 같은 분량인데 비용이 다르게 나오나요, 왜 길어지면 갑자기 느려지나요, 같은 문장을 넣었는데 왜 복원이 어긋나나요. 그래서 이번 글은 저에게는 남의 이야기가 아니었습니다. 매일 쓰는 도구의 바닥을 직접 재본 기록이었죠.

매번 지나치면서도 잘 보지 않던 문이 있다

모델 이야기를 하다 보면 토크나이저는 이미 깔려 있는 전제로 취급되곤 합니다. 어차피 텍스트를 넣으면 숫자로 바뀌고, 숫자가 나오면 다시 텍스트로 돌아오니까요. 그렇다면 왜 이 평범해 보이는 변환을 굳이 따로 떼어 측정해야 할까요?

이유는 위치 때문입니다. 모든 프롬프트는 인코딩을 거쳐야 모델에 들어가고, 모든 생성 결과는 디코딩을 거쳐야 사람에게 돌아옵니다. 다시 말해 토크나이저는 추론 경로 한가운데에 서 있습니다. 모델이 아무리 빨라져도 이 문이 좁으면 전체가 느려지고, 이 문에서 무언가 어긋나면 뒤쪽 계산이 아무리 정확해도 결과가 어긋납니다. 생각해보면 공항 보안 검색대와 비슷합니다. 비행기를 아무리 좋게 만들어도 검색대가 막히면 출발 자체가 늦어지죠.

저는 이 위치라는 관점이 이번 글을 읽는 가장 좋은 출발점이라고 봅니다. 토크나이저를 모델 앞뒤에 붙은 자잘한 유틸리티로 보면 측정이 사치처럼 보입니다. 반면 요청이 반드시 통과해야 하는 관문으로 보면 측정은 당연한 순서가 됩니다. 저희가 새로운 AI 구조를 볼 때도 숫자만 보지는 않습니다. 실제로 무엇이 바뀌었고, 그 변화가 시스템에서 어떤 비용 구조를 만드는지를 함께 보는데요. 토크나이저도 같은 잣대가 필요합니다.

추론 요청이 토크나이저를 통과해 모델로 향하는 흐름을 보여주는 다이어그램
추론 요청이 토크나이저를 통과해 모델로 향하는 흐름을 보여주는 다이어그램

인코딩이 더 비싼 데에는 이유가 있다

겉으로 보면 인코딩과 디코딩은 서로 거울처럼 보입니다. 텍스트를 숫자로 바꾸는 쪽과 숫자를 텍스트로 되돌리는 쪽이니까요. 그런데 실제 작업량은 꽤 다릅니다. 이번 글에서 다룬 구분대로 말하면 인코딩 쪽이 보통 더 많은 일을 합니다. 왜 그럴까요?

인코딩은 들어온 문자열을 그대로 자르면 끝나지 않습니다. 먼저 텍스트를 어떤 단위로 쪼갤지 정하는 사전 토큰화(pre-tokenization)가 필요하고, 그다음에는 어느 조각을 어떻게 합칠지 정하는 병합 순위(merge ranking) 같은 규칙을 따라야 하며, 그 결과를 사전(dictionary)에서 찾아 숫자 식별자로 바꿔야 합니다. 각 단계는 짧아 보여도 문장이 길어지거나 요청이 몰리면 부담이 쌓입니다. 반대로 디코딩은 식별자를 다시 문자열로 펼치는 쪽에 가깝습니다. 물론 디코딩에도 신경 쓸 부분은 있습니다. 다만 단계의 성격 자체가 인코딩보다 가볍게 흘러가는 편이죠.

여기서 재미있는 질문이 생깁니다. 같은 토크나이저인데 왜 한 방향만 유독 무거울까요? 저는 이 비대칭에서 토크나이저의 성격을 읽었습니다. 인코딩은 판단의 연속이고 디코딩은 복원의 연속이기 때문입니다. 판단에는 규칙을 적용하고 모호함을 해소하는 과정이 들어가지만, 복원은 정해진 대응을 따라가는 쪽에 가깝습니다. 그래서 인코딩을 빠르게 만드는 일은 단순히 코드를 다듬는 문제가 아니라 규칙 적용 순서를 어떻게 다듬을지의 문제가 됩니다.

토크나이저 이야기가 아닐 때가 있다

토크나이저 글을 읽다 보면 논의가 옆길로 새는 경우가 있습니다. 토큰 개수가 적게 나오면 무조건 좋은 토크나이저라고 말하거나, 인코딩 속도가 빠르면 모델 전체가 빨라진다고 단정하는 식이죠. 여기서는 조금 조심해서 읽을 필요가 있습니다. 토크나이저 측정이 말해주는 것과 말해주지 않는 것을 가려야 하니까요.

토큰 개수가 적다고 항상 좋은 것은 아닙니다. 짧게 쪼개면 문맥을 더 넓게 쓰는 것처럼 보이지만, 그만큼 한 토큰에 담긴 뜻이 뭉뚱그려질 수 있습니다. 반대로 잘게 쪼개면 뜻은 또렷해져도 같은 문장을 표현하는 데 토큰이 더 많이 들고, 그만큼 컨텍스트와 비용과 대기 시간이 늘어납니다. 그러니까 토큰 효율은 품질과 함께 봐야 하는 숫자입니다. 속도 이야기도 마찬가지입니다. 인코딩이 빨라지면 전체 추론이 가벼워지는 것은 맞지만, 모델 계산이나 네트워크 대기가 더 크면 체감은 제한적일 수 있습니다. 병목이 어디에 있느냐에 따라 같은 개선이 다르게 느껴지죠.

제가 이 구분을 굳이 강조하는 이유는 토크나이저를 모델 품질 논쟁과 섞지 않기 위해서입니다. 이번 글은 어떤 토크나이저가 언어를 더 잘 이해하느냐를 겨루는 글이 아닙니다. 주어진 토크나이저가 인코딩과 디코딩을 얼마나 정확하고 안정적으로 해내는지, 그리고 부하가 걸릴 때 어떻게 행동하는지를 재는 글에 가깝습니다. 이렇게 읽으면 뒤에 나오는 왕복 정확도와 확장 측정이 훨씬 또렷해집니다.

인코딩 안에서는 무슨 일이 벌어질까

인코딩이 비싸다고 말만 들어서는 감이 잘 오지 않습니다. 그래서 머릿속에 작은 흐름도를 그려보는 편이 좋습니다. 문장 하나가 들어오면 토크나이저는 그 문장을 어떤 조각으로 나눌지부터 정합니다. 띄어쓰기나 구두점이나 언어별 경계를 어떻게 다룰지가 여기서 갈립니다. 이 선긋기가 어긋나면 뒤쪽 병합이 아무리 정교해도 출발이 어긋난 셈이 됩니다.

그다음에는 쪼개진 조각들을 규칙에 따라 합치는 과정이 이어집니다. 자주 함께 등장하는 조합을 우선적으로 합치고, 그 우선순위를 따라가며 최종 조각을 확정합니다. 이 순위를 매번 처음부터 헤매면서 정하면 느려질 수밖에 없기 때문에 실제 구현에서는 순서를 미리 정리해두거나 찾는 과정을 빠르게 만드는 장치가 들어갑니다. 마지막으로는 확정된 조각을 숫자로 바꿉니다. 사전에서 해당 조각의 식별자를 찾아내는 과정인데, 말이 쉬워 보이지만 어휘가 크고 요청이 몰리면 이 조회 부담도 무시하기 어렵습니다.

문제는 그다음입니다. 이 세 단계는 따로 놀지 않습니다. 사전 토큰화의 경계가 달라지면 병합 후보가 달라지고, 병합 결과가 달라지면 사전 조회 패턴도 달라집니다. 그래서 인코딩 최적화는 한 단계만 빠르게 만든다고 끝나지 않습니다. 어디서 시간이 쓰이는지를 길이와 배치와 동시성 조건마다 나눠 봐야 합니다. 이번 글이 측정을 길이와 배치와 동시성으로 나눈 것도 같은 맥락으로 읽힙니다. 한 조건에서만 빠르면 실제 운영에서는 다른 얼굴을 보여줄 수 있으니까요.

사전 토큰화와 병합 순위와 사전 조회의 세 단계를 보여주는 다이어그램
사전 토큰화와 병합 순위와 사전 조회의 세 단계를 보여주는 다이어그램

디코딩은 쉬워 보이는데 왜 어긋날까

디코딩은 인코딩보다 단순해 보여서 자칫 가볍게 넘기기 쉽습니다. 숫자를 다시 글자로 펼치면 되지 않나 싶으니까요. 하지만 왕복으로 따져보면 이야기가 달라집니다. 넣은 문장과 돌려받은 문장이 글자 단위로 같아야 하는 장면에서는 작은 어긋남도 눈에 띕니다.

어긋남이 잘 생기는 지점은 대개 세 군데입니다. 하나는 공백입니다. 토큰 경계에 섞인 공백이 합쳐지거나 사라지면 문장의 모양이 미묘하게 달라집니다. 다른 하나는 유니코드입니다. 눈에 같은 글자처럼 보여도 내부 표현이 다르거나, 합성 방식이 달라지면 복원 결과가 달라질 수 있습니다. 나머지 하나는 특수 토큰입니다. 대화 구분이나 제어 신호 같은 토큰은 일반 글자와 다르게 다뤄야 하는데, 이 경계를 잘못 넘나들면 출력에 엉뚱한 조각이 끼거나 필요한 구분이 사라집니다.

여기서 실무적인 장면을 하나 떠올려 보겠습니다. 밤사이 들어온 고객 문의를 그대로 인코딩했다가 디코딩해서 감사 로그에 남기는 파이프라인이 있다고 해보죠. 본문 내용은 맞아 보이는데 공백이나 줄바꿈이 조금씩 달라진다면 어떨까요? 사람은 대충 읽고 넘기겠지만, 뒤쪽에서 그 로그를 해시로 비교하거나 그대로 재입력으로 쓰는 시스템은 곧바로 어긋남을 토해냅니다. 그래서 왕복 정확도(round-trip fidelity)는 예민한 사람들이나 따지는 취향의 문제가 아닙니다. 파이프라인이 길어질수록 조용히 쌓이는 오차의 문제죠. 저는 그래서 디코딩을 인코딩의 짝지 정도로 보지 않습니다. 시스템의 약속을 지키는 쪽으로 봅니다.

길어지고 몰리고 겹치면 병목이 모습을 바꾼다

토크나이저를 한 번만 재면 잘 모르는 것이 있습니다. 부하가 걸릴 때 어디가 먼저 막히는지입니다. 이번 글이 길이와 배치와 동시성을 나란히 놓고 본 것은 그래서 눈에 띕니다. 같은 토크나이저도 조건이 바뀌면 다른 얼굴을 보여주기 때문입니다.

문장이 길어지면 인코딩의 각 단계가 다루는 조각이 늘어납니다. 경계를 나누는 일도 늘고, 순위를 따라 합치는 일도 늘고, 사전에서 찾는 일도 늘어납니다. 배치가 커지면 이야기가 또 달라집니다. 한 요청 안의 길이가 아니라 한 번에 처리해야 하는 요청 묶음이 커지면서 메모리 움직임과 대기 구조가 달라집니다. 그리고 동시성이 올라가면 여러 요청이 서로 얽히면서 스레드나 코어 사이의 조율이 새로운 비용으로 끼어듭니다. 어느 하나만 보고 빠르다고 말하기 어려운 이유가 여기 있습니다.

그렇다면 운영자는 이 측정을 어떻게 써먹을 수 있을까요? 저는 용도별로 보는 지점이 갈린다고 봅니다. 짧은 채팅처럼 요청이 자주 오가는 서비스라면 작은 입력에서의 반응과 동시 요청 처리 흐름이 먼저 눈에 들어옵니다. 긴 문서를 한 번에 넣는 작업이라면 긴 입력에서의 증가 곡선이 더 중요해집니다. 배치 추론처럼 묶어서 처리하는 파이프라인이라면 묶음 크기에 따른 처리량 변화가 관건이죠. 같은 토크나이저라도 어느 장면에 놓느냐에 따라 병목의 위치가 달라지니, 측정도 장면에 맞춰 읽어야 합니다.

길이와 배치와 동시성에 따라 병목이 이동하는 모습을 그린 개념도
길이와 배치와 동시성에 따라 병목이 이동하는 모습을 그린 개념도

토큰 하나가 돈과 시간으로 이어지는 구조

토크나이저 선택이 요금과 속도에 영향을 준다는 말은 자주 듣지만, 그 연결 고리를 차분히 풀어보는 경우는 드뭅니다. 구조를 따라가면 생각보다 직접적입니다. 요금과 상한은 대개 토큰 기준으로 매겨집니다. 같은 분량의 글이라도 토큰으로 세면 길이가 달라지고, 그 길이가 그대로 과금과 컨텍스트 점유로 이어집니다.

처리량(throughput)과 대기 시간(latency)도 같은 뿌리에서 갈라집니다. 인코딩이 느려지면 요청이 모델에 닿기 전에 줄이 서고, 토큰 수가 불어나면 모델이 읽고 써야 하는 양 자체가 늘어납니다. 특히 긴 컨텍스트를 쓰는 작업에서는 토큰 수가 조금만 달라져도 뒤쪽 계산 부담이 함께 움직입니다. 그래서 토크나이저를 고르는 일은 단순히 취향의 문제가 아닙니다. 같은 예산을 어디에 쓸지 고르는 일에 가깝습니다. 토큰을 아끼면 컨텍스트를 넓게 쓰거나 같은 돈으로 더 많은 요청을 처리할 여지가 생기지만, 그 대가로 표현이 뭉개지면 품질 쪽에서 손해가 날 수 있습니다.

(괄호를 열고 잠깐 옆길로 새자면, 저는 이 대목에서 요금 계산기를 먼저 켜기보다 로그를 먼저 봅니다. 실제로 들어오는 문장이 어느 언어에 치우쳐 있는지, 평균 길이는 얼마나 되고, 피크 시간대의 동시 요청은 몇 개인지부터요. 조건 없이 토큰 효율만 비교하면 운영과 동떨어진 결론이 나오기 쉽습니다.) 다시 본론으로 돌아오면, 토크나이저의 속도 숫자는 그래서 단독으로 읽히면 안 됩니다. 어떤 분포의 입력에서, 어떤 묶음 크기로, 어떤 동시성 아래에서 나온 숫자인지가 함께 있어야 합니다. 이번 글처럼 조건을 나눠 재는 방식이 실무에 더 쓸모 있는 이유도 여기 있습니다.

토큰 흐름과 서빙 구조를 함께 보는 엔지니어의 모습을 그린 도식
토큰 흐름과 서빙 구조를 함께 보는 엔지니어의 모습을 그린 도식

기준점이 있어야 다음 이야기가 시작된다

이번 글의 또 다른 역할은 v1이라는 기준점을 세우는 일입니다. 버전 숫자 자체보다 그 뒤의 태도가 더 중요하다고 봅니다. 토크나이저를 감으로 비교하지 않고 같은 조건에서 다시 잴 수 있게 해두겠다는 약속이니까요.

기준점이 있으면 이후의 최적화가 훨씬 또렷해집니다. 구현을 손봐서 인코딩이 빨라졌다면, 그 빨라짐이 어떤 길이에서, 어떤 배치에서, 어떤 동시성에서 나온 것인지를 같은 눈금 위에 올릴 수 있습니다. 반대로 토큰 분할 규칙을 바꿔서 효율이 달라졌다면, 속도 쪽에서 무엇을 내주고 얻은 것인지도 함께 볼 수 있습니다. 이런 식으로 잴 수 있어야 다음 버전 이야기가 설득력을 갖습니다. 그렇지 않으면 매번 다른 조건에서 나온 숫자를 놓고 잘됐다고 말하는 셈이 되니까요.

여기서는 조금 조심해서 읽을 필요가 있습니다. 기준점이 있다고 해서 모든 비교가 공정해지는 것은 아닙니다. 같은 연산량(FLOPs)이라고 같은 하드웨어 비용이나 에너지나 대기 시간을 뜻하지 않듯이, 토크나이저 측정도 무엇을 고정했는지를 밝혀야 합니다. 어떤 하드웨어에서, 어떤 입력 분포로, 어떤 동시성 패턴으로 쟀는지가 빠지면 숫자는 홀로 떠다닙니다. 저는 그래서 이번 v1 측정을 점수표보다 약속으로 읽었습니다. 앞으로의 비교는 이 눈금 위에서 하자는 약속 말이죠.

v1 기준선 위에 이후 버전을 포개어 비교하는 과정을 그린 도식
v1 기준선 위에 이후 버전을 포개어 비교하는 과정을 그린 도식

무엇이 남았고 저는 무엇을 더 보고 싶나

글이 덮은 범위를 넘어서면 자연스럽게 다음 질문이 생깁니다. 실제 시스템 관점에서 보면 다음 질문이 바로 생깁니다. 서로 다른 언어와 도메인에서는 같은 토크나이저가 어떻게 행동할까요? 코드처럼 공백과 기호가 뜻을 가르는 입력과, 일상 대화처럼 짧고 다양한 입력은 같은 곡선을 그릴까요? 저는 이 분포의 문제가 남는다고 봅니다. 평균적인 문장으로만 재면 실제 트래픽의 울퉁불퉁함이 가려지기 때문입니다.

또 하나 보고 싶은 것은 긴 입력과 많은 동시 요청이 겹쳤을 때의 모습입니다. 길이와 배치와 동시성을 따로 재는 것은 출발점으로 좋습니다. 하지만 실제 피크 시간대에는 긴 요청과 짧은 요청이 섞인 채로 몰려듭니다. 이때 스케줄링과 메모리 움직임까지 얽히면 토크나이저의 부담도 다르게 나타날 수 있습니다. 메모리 대역폭이나 서빙 구조까지 함께 보면 숫자의 뜻이 더 분명해지겠죠.

제가 앞으로 보고 싶은 것은 화려한 신기능보다 이런 지루해 보이는 조건표입니다. 어느 하드웨어에서, 어느 분포로, 어느 패턴으로 쟀는지. 그 조건이 쌓여야 토크나이저 이야기가 취향 논쟁에서 운영 지식으로 넘어갑니다. 이번 v1이 그 첫 눈금이라면, 다음에 올 글에서는 눈금 위에 어떤 곡선이 그려질지 궁금합니다.

문을 재는 일이 결국 모델을 이해하는 일이다

여러분도 다음에 요금이나 대기 시간 이야기가 나오면 토큰 수를 한 번 더 들여다보시면 어떨까요. 그 숫자 뒤에 토크나이저의 문이 서 있습니다. 저는 이 글을 덮으면서 새 모델 발표보다 이런 측정 글이 더 반가웠습니다. 발표는 가능성을 보여주지만, 측정은 운영의 바닥을 보여주니까요. 문 하나를 제대로 재는 일이 결국 모델 전체를 이해하는 일로 이어집니다. 다음 버전을 기다리기 전에, 지금 쓰는 토크나이저가 어떤 문인지 먼저 재보면 어떨까요. 그 문을 아는 만큼 다음 선택이 가벼워집니다.

좁은 토크나이저 문이 넓은 모델 영역으로 열리는 모습을 그린 도식
좁은 토크나이저 문이 넓은 모델 영역으로 열리는 모습을 그린 도식

참고 자료

  1. tokenizers v1: encode, decode and scaling, measured · huggingface.co

    리뷰 원문