File size: 5,180 Bytes
56f291f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# External Implementation Notes

Design principles distilled from open-source tokenizers and multimodal
processors. We do not vendor code or weights; we record contracts and
patterns that shape this repo's API.

## HuggingFace tokenizers (`tokenizers.Encoding`)

- Source: https://huggingface.co/docs/tokenizers/api/encoding
- Encoding carries `ids`, `tokens`, `offsets`, `attention_mask`,
  `special_tokens_mask`, `type_ids`, `word_ids`, `sequence_ids`. We mirror
  this contract: `DualTrackResult` exposes `input_ids`, `tokens`,
  `attention_mask`, `special_tokens_mask` plus our routed `track`.
- Special tokens carry `offsets = (0, 0)` to signal "synthetic, not from
  text". We use `(-1, -1)` instead for BOS/EOS to make detection
  unambiguous against zero-length real spans; placeholder tokens (image
  patches) reuse the source `<image>` span and surface `image_index` in
  `metadata`.
- `char_to_token` / `token_to_chars` is feasible only when every token has
  a real character span. We document our offset contract (see
  `offset_contract.md`) so downstream layers can do the same lookups.

## tiktoken

- Source: https://github.com/openai/tiktoken
- BPE state is split into `mergeable_ranks` + a separate `special_tokens`
  dict; specials are matched out-of-band (regex split before BPE). We do
  the same: `vocab.SPECIAL_TOKENS` is reserved in segment 0 and routed
  before language detection.
- Rank-based BPE: merges are stored as `(piece -> rank)` and the lowest
  rank wins at each pair. Our `MorphBPETrainer` records merges in order
  and reconstructs ranks on load — the merge order *is* the rank.
- Byte-level fallback is guaranteed reversible & lossless. We adopt the
  same invariant in `generic_bpe.byte_fallback`: any input byte sequence
  encodes to vocab tokens and decodes back to the exact bytes; emoji
  round-trip is tested.

## SentencePiece

- Source: https://github.com/google/sentencepiece
- Trains directly from raw sentences; whitespace is a regular symbol
  (`▁`). We respect this: `▁` is reserved id 17, decoders treat it as a
  word boundary, and `_strip_hf_boundary_markers` normalizes `▁`/`Ġ`.
- NFKC normalization is part of the tokenizer, not a pre-step the caller
  must remember. Our Rust `normalize_to_nominal_unicode` plays the same
  role for Mongolian and is wired in via the CLI bridge so training
  pipelines cannot skip it.
- Vocabulary size is predetermined and the trainer optimizes inside that
  budget. Our `MorphBPETrainer.train(vocab_size=...)` stops at the target
  size and skips merges below `min_pair_freq` for stability.

## LLaVA processor

- Source: https://huggingface.co/docs/transformers/main/en/model_doc/llava
- The processor expands a single `<image>` placeholder into N image
  tokens based on `patch_size`, `image_seq_length`, and
  `num_additional_image_tokens` (CLS). We mirror this with
  `expand_image_placeholders_by_sizes` driven by `image_patch_count`.
- The processor validates that the text contains the right number of
  image placeholders for the supplied images. We raise `ValueError` when
  `image_sizes` length disagrees with `<image>` count.
- Image embeddings are merged into the LM after tokenization using the
  image-token positions. We therefore must return `image_token_spans` as
  `input_ids`-index ranges (not char ranges) so the model layer can locate
  the slot.

## Qwen2-VL processor

- Source: https://huggingface.co/docs/transformers/main/en/model_doc/qwen2_vl
- Vision tokens are bracketed by explicit start/end markers
  (`<|vision_start|>` / `<|vision_end|>`); the count between them depends
  on `image_grid_thw`. We adopt the same triple
  (`<image_start>`/`<image_patch>*N`/`<image_end>`) and document
  `image_token_spans = (start_index, end_index)` as the *inclusive* token
  range covering all three.
- Video is handled with the same scheme plus a temporal patch factor.
  Our `video_patch_count(num_frames, w, h, patch_size, temporal_patch_size,
  merge_size)` matches the formula
  `ceil(num_frames/temporal_patch_size) * (w/patch_size/merge_size) *
  (h/patch_size/merge_size)`.
- Dynamic resolution: the number of vision tokens is data-dependent.
  Processors must compute counts per-sample, not at config time. Our
  `MultimodalProcessor.__call__` accepts per-call `image_sizes` /
  `video_sizes` rather than baking patch counts into the tokenizer.

## vLLM multimodal processing

- Source: https://docs.vllm.ai/en/latest/features/multimodal_inputs.html
- vLLM enforces that the number of image placeholders in the prompt
  equals the number of supplied images (and exposes
  `limit_mm_per_prompt`). We validate the same invariant in the processor
  and surface a clear `ValueError`.
- The multi-modal data dict is keyed by modality (`"image"`, `"video"`)
  and accepts lists for batched inputs. Our processor accepts
  `images=[...]` / `videos=[...]` in the same shape so future vLLM-style
  serving is a thin wrapper.
- Allowed-media-domains and redirect controls are a security primitive,
  not a tokenizer concern. We note this for the future pipeline layer:
  the tokenizer must never fetch remote bytes itself; callers preload
  PIL images / bytes.