RICCILAB
> projects/study-gpt/document-boundary-utf8-byte-tokenizer
> STUDY-GPT|STATUS: RELEASED|

Stage 04 — BOS가 아니라 문서 경계: UTF-8 byte tokenizer

단일 문서 경계와 순수 UTF-8 byte tokenizer의 정책·비용·실패 경계를 직접 구현하고 검증한다.

_Python

Stage 04부터는 구현 전에 먼저 가설을 세운다. 한번 정한 경계 정책은 encoding, loss target, generation 종료, decode, attention, checkpoint까지 퍼진다. 잘못된 경계 추상화의 Blast Radius가 model 전체인 이유다.

코드: Stage 04 — commit 0af28e8

테스트: Stage 04 테스트

이번 단계가 답할 질문

decoder-only language model에서 문서의 시작과 끝에 같은 token을 사용하는 것이 올바른 추상화일까, 아니면 BOS와 EOS를 분리해야 할까?

이 질문은 token 하나를 아끼는 문제가 아니다.

처음 세운 가설

decoder-only LM이라면 문서 시작과 끝에 같은 경계 token을 쓰는 것이 꽤 좋은 추상화라고 생각한다. data stream 관점에서 문서 사이에는 사실 하나의 경계만 존재한다.

단일 DOC_BOUNDARY token, 일반 text에서 special token 자동 인식 금지, corpus builder의 직접 삽입, byte-level encoding, <UNK> 제거, strict UTF-8 정책, debug decode에서만 <|doc|> 표시, generation seed/stop 공유, checkpoint metadata 검증을 하나의 계약으로 둔다.

핵심 판단은 정확했다. 진짜 비용은 special token 한 줄보다 그 의미를 소비하는 전체 시스템의 정책 복잡도다.

교정 1 — fallback이 아니라 순수 UTF-8 byte tokenizer

character vocabulary에 256개 fallback byte를 섞으면 두 token 공간을 구분하는 protocol이 추가된다. 이번 Stage는 모든 text를 처음부터 UTF-8 byte로 encoding한다.

V={0,1,,255}{256},V=257\mathcal{V}=\{0,1,\ldots,255\}\cup\{256\},\qquad |\mathcal{V}|=257
  • 0..255: 실제 byte 값
  • 256: DOC_BOUNDARY protocol token valid Unicode는 학습 corpus에 없더라도 <UNK> 없이 표현된다.

문서 stream에서 경계는 왜 하나인가

두 문서 ab, ba를 이어 붙이면 corpus stream은 다음과 같다.

[D,97,98,D,98,97,D],D=256[D,97,98,D,98,97,D],\qquad D=256

가운데 DD는 이전 문서의 종료이면서 다음 문서의 시작이다. 문서마다 [D, doc, D]를 이어 붙여 [D,D]를 만드는 대신 corpus builder가 중복 없는 하나의 경계를 삽입한다.

token 계약과 실행 결과
token 계약과 실행 결과

일반 문자열과 protocol token은 다른 입력 경계다

문자열 <|doc|>을 일반 encode에 넣으면 다음 ASCII byte가 된다.

(60, 124, 100, 111, 99, 124, 62)

boundary ID 256은 포함되지 않는다. prompt 내용이 tokenizer control plane을 침범하지 못하며 실제 경계는 corpus builder만 정수 ID로 삽입한다.

special token 표기는 일반 text다
special token 표기는 일반 text다

교정 2 — decode 책임을 분리한다

  • decode_text(): byte token만 허용하고 special token이면 실패
  • decode_document(): 정확히 바깥쪽의 boundary만 제거하고 내부 boundary는 거부
  • debug_decode(): debug 화면에서만 <|doc|>와 replacement character를 표시 byte 0xFF는 보존 가능한 raw data지만 단독으로 valid UTF-8 text는 아니다. decode_bytes()는 원본을 보존하고 decode_text()는 실패한다.

raw bytes와 strict text의 차이
raw bytes와 strict text의 차이

Unicode 길이와 계산 비용

문자 수를 NN, UTF-8 byte token 수를 TT라 하면 valid Unicode에서

NT4NN\le T\le4N

이다. 안녕은 2문자이지만 6 token이다. vocabulary 크기를 VV, embedding width를 DD라 하면 token embedding과 output head의 parameter 수는

Ptoken=2VDP_{\mathrm{token}}=2VD

이다. D=16D=16일 때 shared boundary는 8224개, BOS/EOS 분리는 8256개로 차이는 32개뿐이다. 그러나 full causal attention은 대략 O(T2D)O(T^2D)이므로 한 문자가 4 byte가 되는 최악의 비교에서는 위치 비용이 최대

(4N)2DN2D=16\frac{(4N)^2D}{N^2D}=16

배가 될 수 있다. token 하나의 parameter 비용과 byte tokenization의 model 전체 비용은 분리해서 판단해야 한다.

중요한 한계 — boundary는 attention barrier가 아니다

DOC_BOUNDARY가 있어도 causal attention은 이전 문서를 읽을 수 있다. 독립 문서가 필요하면 sample/KV cache reset, document segment attention mask, 또는 cross-document context 허용 정책을 별도로 선택해야 한다. boundary는 의미 protocol이지 memory 격리 장치가 아니다.

generation과 checkpoint 계약

generation은 (256,)을 seed로 넣되 이미 존재하는 seed에서 멈추지 않고, 새로 sampling한 token이 256일 때만 종료한다.

stop(tnew)=[tnew=256]\operatorname{stop}(t_{\mathrm{new}})=[t_{\mathrm{new}}=256]

checkpoint는 tokenizer version utf8-byte-v1, vocabulary size 257, boundary ID 256, canonical vocabulary SHA-256이 모두 일치해야 load된다.

비용과 Blast Radius

corpus builder는 문서 수, encoding 전 character 수, encoding 후 byte 수, 전체 stream token 수를 각각 제한한다. invalid Unicode, 내부 boundary, invalid UTF-8, vocabulary contract 불일치는 각 입력 경계에서 즉시 실패한다.

직접 실행한 결과

python .\stages\stage_04_byte_tokenizer.py
python -m unittest tests.test_stage_04_byte_tokenizer -v
  • Stage 04 테스트 12개 통과
  • 전체 테스트 44개 통과
  • sanity check: PASS
    Stage 04 테스트
    Stage 04 테스트

이번 Stage가 증명한 것

단일 document boundary는 decoder-only stream의 시작과 끝을 함께 표현하는 올바른 추상화가 될 수 있다. 다만 byte 방식은 <UNK>를 없애는 대신 sequence와 model 계산 비용을 늘리며, boundary 자체는 attention isolation을 제공하지 않는다.

Stage 05의 질문은 “tensor가 아직 없을 때 shape는 주석인가, 실행 시 반드시 검사해야 하는 계약인가?”이다.

EOF 2026-07-11