ಯೂನಾಗ್ರಾಮ್ ಟೋಕನೈಸರ್ CPU ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸುವುದು
ನಾವು ಕಾರ್ಯಕ್ಷಮತೆಯ ವಿಶೇಷ ಯೋಜನೆಯಾಗಿ ನಮ್ಮ ಯೂನಾಗ್ರಾಮ್ ಟೋಕನೈಸರ್ ಅನ್ನು ಮೊದಲಿನಿಂದ ಮರುರೂಪಿಸಿದ್ದೇವೆ.
ನಾವು ಕಾರ್ಯಕ್ಷಮತೆಯ ವಿಶೇಷ ಯೋಜನೆಯಾಗಿ ನಮ್ಮ ಯೂನಾಗ್ರಾಮ್ ಟೋಕನೈಸರ್ ಅನ್ನು ಮೊದಲಿನಿಂದ ಮರುರೂಪಿಸಿದ್ದೇವೆ. ಉತ್ಪಾದನಾ ಇನ್ಪುಟ್ ಉದ್ದಗಳಲ್ಲಿ, ಹೊಸ ಎಕೋಡರ್ ಹಗ್ಗಿಂಗ್ ಫೇಸ್ ಟೋಕನೈಸರ್ಸ್ ಕ್ರೇಟ್ಗೆ ಹೋಲಿಸಿದರೆ p50 ಸುಪ್ತತೆಯನ್ನು ಸ Roughly 5×, ಸೆಂಟೆನ್ಸ್ಪೀಸ್ (C++) ಗೆ ಹೋಲಿಸಿದರೆ ~2×, ಮತ್ತು IREE ಯ ಟೋಕನೈಸರ್ (C) ಗೆ ಹೋಲಿಸಿದರೆ ~1.5× ಕಡಿಮೆಗೊಳಿಸುತ್ತದೆ, ಜೊತೆಗೆ ಸ್ಥಿರ-ಸ್ಥಿತಿಯ ಹೀಪ್ ಹಂಚಿಕೆಗಳು ಶೂನ್ಯವಾಗಿವೆ. ಉತ್ಪಾದನೆಯಲ್ಲಿ ಇದು ನಮ್ಮ ಇನ್ಫರೆನ್ಸ್ ಸ್ಟ್ಯಾಕ್ನಲ್ಲಿ CPU ಬಳಕೆಯನ್ನು 5-6× ಕಡಿಮೆ ಮಾಡಿದೆ ಮತ್ತು ರಿರಾಂಕರ್ ಸುಪ್ತತೆಯಲ್ಲಿ ದಶಕಗಳ ಮಿಲಿಸೆಕೆಂಡ್ಗಳನ್ನು ಕಡಿಮೆ ಮಾಡಿದೆ.
ಪ್ರೇರಣೆ
LLM ಇನ್ಫರೆನ್ಸ್ ವೆಚ್ಚವನ್ನು ಸಾಮಾನ್ಯವಾಗಿ GPU ಕೆಲಸವಾಗಿ ರೂಪಿಸಲಾಗುತ್ತದೆ: KV ಸಂಗ್ರಹಣೆಗಳು, ಗಮನ ಕರ್ನಲ್ಗಳು, ಎಕ್ಸ್ಪರ್ಟ್ ರೂಟಿಂಗ್. ಆದರೆ ಉತ್ಪಾದನಾ ಟ್ರಾಫಿಕ್ನ ಅರ್ಥಪೂರ್ಣ ಭಾಗವು ತುಂಬಾ ಚಿಕ್ಕ ಮಾದರಿಗಳಲ್ಲಿ ಚಲಿಸುತ್ತದೆ, ಇದು ಮುಂಚೂಣಿಯ ಟ್ರಾನ್ಸ್ಫಾರ್ಮರ್ಗಳಿಗಿಂತ ಎರಡು ಮೂರು ಆರ್ಡರ್ ಆಫ್ ಮ್ಯಾಗ್ನಿಟ್ಯೂಡ್ ಚಿಕ್ಕದಾಗಿದೆ. ಎಂಬೆಡಿಂಗ್ ಮಾದರಿಗಳು, ಕ್ಲಾಸಿಫೈಯರ್ಗಳು ಮತ್ತು ರಿರಾಂಕರ್ಗಳು ಇನ್ನೂ GPU ಗಳಲ್ಲಿ ಚಲಿಸುತ್ತವೆ, ಆದರೆ ಫಾರ್ವರ್ಡ್ ಪಾಸ್ ತುಂಬಾ ಚಿಕ್ಕದಾಗಿದ್ದು ಅದರ ಸುತ್ತಲಿನ ಎಲ್ಲವೂ ಮುಖ್ಯವಾಗಲು ಪ್ರಾರಂಭಿಸುತ್ತದೆ.
ಅತಿ ಸ್ಪಷ್ಟವಾದ ಉದಾಹರಣೆ ಎಂದರೆ ಪ್ರತಿ ವಿನಂತಿಗೆ ನೂರಾರು ಅಭ್ಯರ್ಥಿ ದಾಖಲೆಗಳನ್ನು ಸ್ಕೋರ್ ಮಾಡುವ reranker. ಸಣ್ಣ ಮಾದರಿಯೊಂದಿಗೇ, GPU ಗಣನೆ ಸಾಮಾನ್ಯವಾಗಿ ಏಕಾಂಕ ಮಿಲಿಸೆಕೆಂಡುಗಳಲ್ಲಿ ಮುಗಿಯುತ್ತದೆ, ಆದರೆ ಪ್ರತಿ ಇನ್ಪುಟ್ ಇನ್ನೂ ಮೊದಲು CPU-ಪಕ್ಷದ tokenization ಮೂಲಕ ಹಾದುಹೋಗಬೇಕು; ಇದರಿಂದ ಪಠ್ಯ vocabulary ID ಗಳಾಗಿ ಪರಿವರ್ತಿತವಾಗುತ್ತದೆ. ಬ್ಯಾಚ್ ದೊಡ್ಡದಾಗಿರುವಾಗ, ಅದು ಒಟ್ಟು ವಿನಂತಿ ವಿಳಂಬದ ಅರ್ಥಪೂರ್ಣ ಭಾಗವನ್ನು ಹೊಂದಿರಬಹುದು.
ಈ ಕೆಲಸವು SentencePiece ನೊಂದಿಗೆ ತರಬೇತಿ ಪಡೆದ 250K-ಟೋಕನ್ ಯೂನಾಗ್ರಾಮ್ ಶಬ್ದಕೋಶವಾದ XLM-RoBERTa ಅನ್ನು ಗುರಿಯಾಗಿಸುತ್ತದೆ. ಫೈನ್-ಟ್ಯೂನ್ ಮಾಡಿದ RoBERTa-ಕುಟುಂಬದ ಎಕೋಡರ್ಗಳು ರ್ಯಾಂಕಿಂಗ್, ರಿಟ್ರೈವಲ್ ಮತ್ತು ಹೋಲಿಕೆ ಕಾರ್ಯಗಳಿಗೆ ಸಾಮಾನ್ಯ ಆಯ್ಕೆಯಾಗಿದೆ. ನಾವು ಎಲ್ಲೆಡೆ ಸಿಂಗಲ್-ಥ್ರೆಡ್ ಪ್ರತಿ-ಎಕೋಡ್ ಸುಪ್ತತೆಯನ್ನು ವರದಿ ಮಾಡುತ್ತೇವೆ; ಉತ್ಪಾದನೆಯಲ್ಲಿ ನಾವು rayon ಬಳಸಿ ಕೋರ್ಗಳಾದ್ಯಂತ ಸಮಾನಾಂತರಗೊಳಿಸುತ್ತೇವೆ.
ಈ ಲೇಖನದಾದ್ಯಂತ ನಾವು Hugging Faceನ taggerizers crate ಅನ್ನು ನಾವು ಹೋಲಿಕೆಗಾಗಿ ಮಾನದಂಡವಾಗಿ ಪರಿಗಣಿಸುತ್ತೇವೆ, ಏಕೆಂದರೆ ಅದು ಹೆಚ್ಚಿನ ತಂಡಗಳು ಆಯ್ಕೆಮಾಡುವ ಡೀಫಾಲ್ಟ್ Rust tokenizer ಆಗಿದೆ. SentencePiece (C++) ಮತ್ತು IREE (C) ಇತರ ಎರಡು ಪ್ರಮುಖ ಉತ್ಪಾದನಾ ಅನುಷ್ಠಾನಗಳಾಗಿ ಹೋಲಿಕೆಗಾಗಿ ಸೇರಿಸಲಾಗಿದೆ.
ವಿಟೆರ್ಬಿ ಅಲ್ಗಾರಿದಮ್
2018 ರಲ್ಲಿ ಕುಡೊ ಅವರು ಪರಿಚಯಿಸಿದ ಯೂನಾಗ್ರಾಮ್ ಟೋಕನೈಸೇಷನ್ (Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates) ಮತ್ತು SentencePiece ನಲ್ಲಿ ರವಾನಿಸಲಾಗಿದೆ, ಇದು ವಿಭಜನೆಯನ್ನು ಅತ್ಯಂತ-ಸಂಭಾವ್ಯ-ಪಥದ ಸಮಸ್ಯೆಯಾಗಿ ರೂಪಿಸುತ್ತದೆ: ಪ್ರತಿ ಶಬ್ದಕೋಶದ ಟೋಕನ್ ಕಲಿತ ಲಾಗ್-ಸಂಭಾವ್ಯತೆಯನ್ನು ಹೊಂದಿದೆ, ಮತ್ತು ಎಕೋಡರ್ ಟೋಕನ್ ಸ್ಕೋರ್ಗಳು ಹೆಚ್ಚಿನ ಮೌಲ್ಯಕ್ಕೆ ಮೊತ್ತವಾಗುವ ವಿಭಜನೆಯನ್ನು ಆಯ್ಕೆ ಮಾಡುತ್ತದೆ. ಈ ಸಮಸ್ಯೆಯನ್ನು Viterbi algorithm (1967) ಮೂಲಕ ಪರಿಹರಿಸಲಾಗಿದೆ, ಇದರಲ್ಲಿ ಬೈಟ್ ಸ್ಥಾನಗಳು ಗ್ರಾಫ್ ಪದರುಗಳಾಗಿ ಮತ್ತು ಶಬ್ದಕೋಶದ ಟೋಕನ್ಗಳು ನಿರಂತರ ಬೈಟ್ ಶ್ರೇಣಿಯನ್ನು ವ್ಯಾಪಿಸುವ ಅಂಚುಗಳಾಗಿರುತ್ತವೆ. ಮರುಕಳಿಸುವಿಕೆಯು ಬೈಟ್ ಸ್ಥಾನಗಳ ಮೇಲೆ ಒಂದು ಆಯಾಮದ DP ಆಗಿದೆ:
best = [-inf] * (n + 1)
best[0] = 0
back = [None] * (n + 1)
for i in 0..n:
if best[i] == -inf:
continue
if byte i is a UTF-8 continuation byte:
continue
walk the vocabulary trie from byte position i:
for each matching token t ending at position j:
cand = best[i] + score(t)
if cand > best[j]:
best[j] = cand
back[j] = (i, token_id(t))
if no token matched, fall back to <unk>
backtrack from position n using back[] to recover token IDsಹೊರಗಿನ ಲೂಪ್ ಇನ್ಪುಟ್ ಉದ್ದದಲ್ಲಿ ರೇಖೀಯವಾಗಿದೆ, ಪ್ರತಿ ಸ್ಥಾನದಲ್ಲಿ ದೀರ್ಘವಾದ ಶಬ್ದಕೋಶದ ಟೋಕನ್ನಿಂದ (XLM-RoBERTa ಗಾಗಿ 48 UTF-8 ಬೈಟ್ಗಳು) ಸೀಮಿತಗೊಳಿಸಲಾಗಿದೆ. ಒಳಗಿನ ಟ್ರೈ ವಾಕ್ ಹಾಟ್ ಪಾತ್ ಆಗಿದೆ: 16k-ಟೋಕನ್ ಇನ್ಪುಟ್ನಲ್ಲಿ, ಇದು ಲಕ್ಷಾಂತರ ಪರಿವರ್ತನೆಗಳನ್ನು ಚಲಾಯಿಸುತ್ತದೆ ಮತ್ತು ಪ್ರತಿ ಎಕೋಡ್ಗೆ ಕೋಟ್ಯಂತರ ಸೂಚನೆಗಳನ್ನು ನಿವೃತ್ತಿಗೊಳಿಸುತ್ತದೆ.
ಉಲ್ಲೇಖ ಅನುಷ್ಠಾನ
Hugging Face ನ tokenizers v0.22 ಮರುಕಳಿಸುವಿಕೆಯನ್ನು ನೇರವಾಗಿ ಕಾರ್ಯಗತಗೊಳಿಸುತ್ತದೆ, ರಿವರ್ಸ್ ಲುಕ್ಅಪ್ಗಾಗಿ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಮತ್ತು ಪ್ರತಿಯೊಂದು ಟ್ರೈ ನೋಡ್ನಲ್ಲಿ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್-ಮಕ್ಕಳೊಂದಿಗೆ:
vocab: Vec<(String, f64)> // indexed by token ID
token_to_ids: AHashMap<String, u32> // reverse lookup
trie: Trie<u8> // byte-level prefix trie
struct Node<Label> {
is_leaf: bool,
children: AHashMap<Label, Node<Label>>, // heap-allocated map per node
}ಮತ್ತು ಒಳಗಿನ ಲೂಪ್ (ಸರಳೀಕೃತ) - ಪ್ರತಿ ಪ್ರಿಫಿಕ್ಸ್-ಹುಡುಕಾಟ ಹೊಂದಾಣಿಕೆಯು ಹೊಸ ಸ್ಟ್ರಿಂಗ್ ಅನ್ನು ಹಂಚಿಕೆ ಮಾಡುತ್ತದೆ ಮತ್ತು ನಂತರ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಅನ್ನು ಪರಿಶೋಧಿಸುತ್ತದೆ:
fn encode_optimized(&self, sentence: &str) -> Result<Vec<String>> {
// alloc
let mut best = vec![BestPathNode::default(); sentence.len() + 1];
let mut starts_at = 0;
while starts_at < sentence.len() {
for tok_bytes in self.trie.common_prefix_search(sentence.bytes().skip(starts_at)) {
// alloc per match
let token: String = String::from_utf8(tok_bytes).unwrap();
// hash lookup
let id = self.token_to_ids.get(&token).unwrap();
let score = self.vocab[*id as usize].1;
// DP update at best[starts_at + tok_bytes.len()]
// ...
}
starts_at += mblen;
}
// backtrace into Vec<String>, one allocation per output token
}ಕೋಡ್ ಓದಬಲ್ಲ ರಸ್ಟ್ ಆಗಿದೆ, ಆದರೆ ಮೂಲ ಮಟ್ಟದಲ್ಲಿ ಅದೃಶ್ಯವಾಗಿರುವ ಸಾಕಷ್ಟು ಕೆಲಸಗಳು ನಡೆಯುತ್ತವೆ. ಸಮಯ ಎಲ್ಲಿಗೆ ಹೋಗುತ್ತದೆ ಎಂಬುದನ್ನು ಮುಂದಿನ ವಿಭಾಗವು ವಿವರಿಸುತ್ತದೆ.
ಪೂರ್ವಸಂಸ್ಕರಣೆಯ ಕುರಿತು ಒಂದು ಟಿಪ್ಪಣಿ
ವಿಟೆರ್ಬಿ ಚಾಲನೆಗೊಳ್ಳುವ ಮೊದಲು, ಇನ್ಪುಟ್ ಯು ನಿಕೋಡ್ ಸಾಮಾನ್ಯೀಕರಣ (NFKC) ಮತ್ತು ಪದ-ಗಡಿ ಗುರುತು ಮಾಡುವಿಕೆಯ ಮೂಲಕ ಹೋಗುತ್ತದೆ. ಪ್ರೊಫೈಲಿಂಗ್ ನಮ್ಮ ದೃಶ್ಯಗಳಲ್ಲಿ ಒಟ್ಟು ಎಕೋಡ್ ಸಮಯದ 5% ಕ್ಕಿಂತ ಕಡಿಮೆ ಪೂರ್ವಭಾವಿ ಸಂಸ್ಕರಣೆಯನ್ನು ಇರಿಸುತ್ತದೆ, ಆದ್ದರಿಂದ ನಾವು ಪೋಸ್ಟ್ನ ಉಳಿದ ಭಾಗಕ್ಕಾಗಿ ವಿಟೆರ್ಬಿ ಫಾರ್ವರ್ಡ್ ಪಾಸ್ ಮೇಲೆ ಗಮನಹರಿಸುತ್ತೇವೆ.
ಉಲ್ಲೇಖ ಅನುಷ್ಠಾನವು ತನ್ನ ಸಮಯವನ್ನು ಎಲ್ಲಿ ಕಳೆಯುತ್ತದೆ
CPU ಕೋಡ್ ಅನ್ನು ವೇಗವಾಗಿ ಮಾಡಲು ಎರಡು ಮಾರ್ಗಗಳಿವೆ: ಕಡಿಮೆ ಸೂಚನೆಗಳನ್ನು ಕಾರ್ಯಗತಗೊಳಿಸಿ, ಅಥವಾ ಹೆಚ್ಚಿನ IPC ಅನ್ನು ಪಡೆಯಿರಿ. Cachegrind ಮೊದಲನೆಯದನ್ನು ಅಳೆಯುತ್ತದೆ, perf stat ಎರಡನೆಯದನ್ನು ಅಳೆಯುತ್ತದೆ. ಈ ಪೋಸ್ಟ್ನಲ್ಲಿರುವ ಪ್ರತಿಯೊಂದು ಆಪ್ಟಿಮೈಸೇಶನ್ ಒಂದಲ್ಲ ಒಂದು ಕಡೆ ಬರುತ್ತದೆ.
CPUಗಳು ನಾಲ್ಕು ವಿಷಯಗಳಲ್ಲಿ ನಿಲುಕುತ್ತವೆ: cache missಗಳು (L1d ~4 cycles, L3-miss-to-DRAM ನೂರಾರು cycles), out-of-order engine ಸಮಾಂತರಗೊಳಿಸಲಾಗದ dependent-load chains, branch mispredictಗಳು (ಪ್ರತಿ ತಪ್ಪು ಊಹೆಗೆ ಸುಮಾರು ~15-20 cycles flush ಆಗುತ್ತವೆ), ಮತ್ತು allocator calls. reference unigram engine ಅನುಷ್ಠಾನವು ಈ ನಾಲ್ಕೂ ಅಂಶಗಳನ್ನು ಸ್ಪರ್ಶಿಸುತ್ತದೆ:
ಬಾಟಲ್ ನೆಕ್ | ಯಾಂತ್ರಿಕತೆ | ಅಳೆದ ಪರಿಣಾಮ |
ಪ್ರತಿ ಹೊಂದಾಣಿಕೆಗೆ ಹಂಚಿಕೆ | ಹೊಂದಾಣಿಕೆಯಾದ ಬೈಟ್ಗಳ ಮೇಲೆ String::from_utf8 ಜೊತೆಗೆ ಪ್ರತಿ ಟ್ರೈ ಹೊಂದಾಣಿಕೆಗೆ AHashMap ಲುಕ್ಅಪ್; ಟೋಕನ್ ID ಈಗಾಗಲೇ ಲೀಫ್ನಲ್ಲಿದೆ, ಆದರೆ ಉಲ್ಲೇಖವು ಅದನ್ನು ಅಲ್ಲಿ ಸಂಗ್ರಹಿಸುವುದಿಲ್ಲ | 512 ಟೋಕನ್ಗಳಲ್ಲಿ 7,295 ಅಲೋಕ್ಸ್, 1k ನಲ್ಲಿ 14,542, 16k ನಲ್ಲಿ 299,171 |
ಪ್ರತಿ ಬೈಟ್ಗೆ ಪಾಯಿಂಟರ್ ಚೇಸ್ | ಪ್ರತಿ ಟ್ರೈ ನೋಡ್ನಲ್ಲಿ AHashMap, ಮುಂದಿನ ಬೈಟ್ ಅನ್ನು ಹ್ಯಾಶ್ ಮಾಡುವುದು ಮತ್ತು ಪ್ರತಿ ಹಂತಕ್ಕೆ ಎರಡು ಪಾಯಿಂಟರ್ಗಳನ್ನು ಡಿರೆಫರೆನ್ಸ್ ಮಾಡುವುದು; Node ಸ್ಟ್ರಕ್ಟ್ ಕೋಲ್ಡ್ ಫೀಲ್ಡ್ಗಳಾದ is_leaf ಅನ್ನು ಸಹ ಬಂಡಲ್ ಮಾಡುತ್ತದೆ, ಪ್ರತಿ ಭೇಟಿಯ დროს ಅವುಗಳನ್ನು ಕ್ಯಾಶ್ಗೆ ಎಳೆಯುತ್ತದೆ | L1d ಮಿಸ್ ದರವು ಕಡಿಮೆಯಿರುತ್ತದೆ (1k ನಲ್ಲಿ ~2.5%) ಏಕೆಂದರೆ ಅಲೋಕೇಟರ್ ಹಾಟ್ ಪುಟಗಳು L1 ನಲ್ಲಿ ವಾಸಿಸುತ್ತವೆ; ವೆಚ್ಚವು ಅವಲಂಬಿತ-ಲೋಡ್ ಸುಪ್ತತೆಯಾಗಿ ತೋರಿಸುತ್ತದೆ |
ದೀರ್ಘ ಇನ್ಪುಟ್ಗಳಲ್ಲಿ L2 ಥ್ರ್ಯಾಶಿಂಗ್ | DP ಕೋಷ್ಟಕ, ಪ್ರಿಫಿಕ್ಸ್ ಸ್ಕ್ರ್ಯಾಚ್, ಮತ್ತು ಔಟ್ಪುಟ್ ಟೋಕನ್ ಪಟ್ಟಿ ಎಲ್ಲವೂ ಪ್ರತಿ ಕರೆಯಲ್ಲೂ ಹೊಸದಾಗಿ ಹಂಚಿಕೆ ಮಾಡಲ್ಪಡುತ್ತವೆ | ಸಂಚಿತ ಅಲೋಕೇಶನ್ ಹೆಜ್ಜೆಗುರುತು ಪ್ರತಿ-ಕೋರ್ 2 MB L2 ಅನ್ನು ಮೀರಿದಂತೆ L2 ಮಿಸ್ ದರವು 128 ಟೋಕನ್ಗಳಲ್ಲಿ 12% ರಿಂದ 16k ನಲ್ಲಿ 50% ಕ್ಕೆ ಏರುತ್ತದೆ |
ರೆಫರೆನ್ಸ್ ಟೋಕನೈಜರ್ನ ಬಾಟಲ್ನೆಕ್ಗಳು, ಕಾರ್ಯವಿಧಾನಗಳು, ಮತ್ತು ಅಳತೆಗತ ಪರಿಣಾಮಗಳು.
ಪ್ರಮುಖ ವೆಚ್ಚವೆಂದರೆ ಪ್ರತಿ ಹೊಂದಾಣಿಕೆಗೆ ಹಂಚಿಕೆ. 1k ಟೋಕನ್ಗಳಲ್ಲಿ, ಉಲ್ಲೇಖ ಅನುಷ್ಠಾನವು ಪ್ರತಿ ಎಕೋಡ್ಗೆ ಸುಮಾರು 7M ಸೂಚನೆಗಳನ್ನು 2.8 ರ IPC ನಲ್ಲಿ ನಿವೃತ್ತಿಗೊಳಿಸುತ್ತದೆ (ಪೂರ್ಣಗೊಳಿಸುತ್ತದೆ), ಅವುಗಳಲ್ಲಿ ಹೆಚ್ಚಿನವು ಅಲೋಕೇಟರ್ ಕರೆಗಳು ಮತ್ತು ಸ್ಟ್ರಿಂಗ್ ಪರಿವರ್ತನೆಗಳಲ್ಲಿ. ಸಂಚಿತ ಅಲೋಕೇಶನ್ ಹೆಜ್ಜೆಗುರುತು ಪ್ರತಿ-ಕೋರ್ ಕ್ಯಾಶ್ ಅನ್ನು ಓವರ್ಫ್ಲೋ ಮಾಡಿದ ನಂತರ, L2 ಮಿಸ್ಗಳು ~1k ಟೋಕನ್ಗಳನ್ನು ಮೀರಿದ ನಂತರವೇ ಮುಖ್ಯವಾಗುತ್ತವೆ.
ಪ್ರತಿ-ಟೋಕನ್ ಹಂಚಿಕೆಯು ಸ್ಥಿರವಾಗಿದೆ: ಇನ್ಪುಟ್ ಗಾತ್ರವನ್ನು ಲೆಕ್ಕಿಸದೆ ಸುಮಾರು 2 KB ಮತ್ತು ಪ್ರತಿ ಟೋಕನ್ಗೆ ~18 ಹಂಚಿಕೆಗಳು. ಸುಪ್ತತೆ ಅರೇಖೀಯತೆಯು L2 ನಿಂದ ಬರುತ್ತದೆ: ಸಂಚಿತ ಬೈಟ್ಗಳು 1k ಟೋಕನ್ಗಳ ಸುತ್ತಲೂ 2 MB ಅನ್ನು ದಾಟುತ್ತವೆ, ಮಿಸ್ ದರವು 8% ರಿಂದ ~50% ಕ್ಕೆ ಏರುತ್ತದೆ, ಮತ್ತು p99 ಕರ್ವ್ ಹೊಂದಿಸಲು ಬಾಗುತ್ತದೆ.
ನಾವು ಹೇಗೆ ಅಳೆದಿದ್ದೇವೆ
ನಾವು ಮೂರು ಉತ್ಪಾದನಾ ಟೋಕನೈಸರ್ಗಳನ್ನು ಹೋಲಿಸುತ್ತೇವೆ, ಎಲ್ಲವೂ ಒಂದೇ XLM-RoBERTa ಮಾದರಿಯ ವಿರುದ್ಧ ಒಂದೇ ಯೂನಾಗ್ರಾಮ್ ಅಲ್ಗಾರಿದಮ್ ಅನ್ನು ಚಲಾಯಿಸುತ್ತವೆ:
ಎಂಜಿನ್ | ಭಾಷೆ | ಟ್ರೈ |
Hugging Face | Rust | HashMap-ಮಕ್ಕಳ ಪ್ರಿಫಿಕ್ಸ್ ಟ್ರೈ |
SentencePiece | C++ | Darts ಡಬಲ್-ಅರೇ ಟ್ರೈ |
IREE ಟೋಕನೈಸರ್ | C | Darts ಡಬಲ್-ಅರೇ ಟ್ರೈ |
ಹೋಲಿಕೆಯಲ್ಲಿ ಬಳಸಲಾದ ಟೋಕನೈಜರ್ ಅನುಷ್ಠಾನಗಳು, ಭಾಷೆಗಳು, ಮತ್ತು ಟ್ರೈ ಪ್ರತಿನಿಧಾನಗಳು.
ನಾವು SentencePiece ಮತ್ತು IREE ಅನ್ನು ಅವುಗಳ ರಸ್ಟ್ ರಾಪರ್ಗಳ ಬದಲಿಗೆ ನೇರ C/C++ FFI ಮೂಲಕ ಲಿಂಕ್ ಮಾಡುತ್ತೇವೆ; ರಾಪರ್ಗಳು ಪ್ರತಿ-ಕರೆ ಹಂಚಿಕೆಗಳು ಮತ್ತು ಸ್ಟ್ರಿಂಗ್ ಪರಿವರ್ತನೆಗಳನ್ನು ಸೇರಿಸುತ್ತವೆ ಅದು ಆಧಾರವಾಗಿರುವ ಲೈಬ್ರರಿಯ ವೆಚ್ಚದ ಭಾಗವಲ್ಲ.
ಎಲ್ಲಾ ಎಂಜಿನಗಳನ್ನು ಸಂಪೂರ್ಣ ಆಪ್ಟಿಮೈಸೇಶನ್ಗಳೊಂದಿಗೆ ನಿರ್ಮಿಸಲಾಗಿದೆ (O3, ಸ್ಥಳೀಯ ಗುರಿಪಡಿಸುವಿಕೆ, ಲಿಂಕ್-ಟೈಮ್ ಆಪ್ಟಿಮೈಸೇಶನ್). Intel Xeon Platinum 8488C ನಲ್ಲಿ ಒಂದು ಶಾಂತ ಕೋರ್ಗೆ ಪಿನ್ ಮಾಡಲಾದ ಅಳತೆಗಳು ಚಲಿಸುತ್ತವೆ. ಪ್ರತಿ ಎಂಜಿನ್ ಮತ್ತು ಇನ್ಪುಟ್ ಗಾತ್ರಕ್ಕಾಗಿ ನಾವು ವಾಲ್-ಕ್ಲಾಕ್ ಸುಪ್ತತೆ (p50 ಮತ್ತು p99, 1,000 ವಾರ್ಮಪ್ ನಂತರ 10,000 ಪುನರಾವರ್ತನೆಗಳು), Valgrind cachegrind ಮೂಲಕ ನಿವೃತ್ತಿಗೊಂಡ ಸೂಚನೆಗಳು, perf stat ಹಾರ್ಡ್ವೇರ್ ಕೌಂಟರ್ಗಳಿಂದ IPC ಮತ್ತು ಕ್ಯಾಶ್ ಮಿಸ್ ದರಗಳು, ಮತ್ತು ಅಳೆದ ವಿಂಡೋದ ಸುತ್ತಲಿನ ಜಾಗತಿಕ ಅಲೋಕೇಟರ್ ರಾಪರ್ನಿಂದ ಎಣಿಕೆ ಮಾಡಲಾದ ಹೀಪ್ ಅಂಚಿಕೆಗಳನ್ನು ಸಂಗ್ರಹಿಸುತ್ತೇವೆ.
514 ಟೋಕನ್ಗಳಲ್ಲಿ (512 ಕ್ಕಿಂತ +2 ಎಂಬುದು BOS/EOS ಇಂಜೆಕ್ಷನ್ ಆಗಿದೆ):
ಅನುಷ್ಠಾನ | p50 ಸುಪ್ತತೆ | ಸೂಚನೆಗಳು | IPC | ಹಂಚಿಕೆಗಳು |
Hugging Face | 349 µs | 3.60M | 2.89 | 7,295 |
SentencePiece | 128 µs | 1.83M | 3.39 | 1,559 |
IREE | 112 µs | 2.28M | 3.06 | 1 |
514 ಟೋಕನ್ಗಳಲ್ಲಿ ಟೋಕನೈಜರ್ ವಿಳಂಬ, ಸೂಚನಾ ಎಣಿಕೆ, IPC, ಮತ್ತು ಅಲೊಕೇಷನ್ಗಳು.
SP ಮತ್ತು IREE ಉಲ್ಲೇಖದ ಸುಪ್ತತೆಯ ಸುಮಾರು ಮೂರನೇ ಒಂದು ಭಾಗಕ್ಕೆ ಭೂಮಿ ಮಾಡುತ್ತವೆ. ಅವು ಸುಮಾರು ಅರ್ಧದಷ್ಟು ಸೂಚನೆಗಳನ್ನು ನಿವೃತ್ತಿಗೊಳಿಸುತ್ತವೆ, ಹೆಚ್ಚಿನ IPC ನಲ್ಲಿ ಚಲಿಸುತ್ತವೆ, ಮತ್ತು ಒಂದು ಅಥವಾ ಎರಡು ಆರ್ಡರ್ ಆಫ್ ಮ್ಯಾಗ್ನಿಟ್ಯೂಡ್ ಕಡಿಮೆ ಹಂಚಿಕೆ ಮಾಡುತ್ತವೆ.
SentencePiece ಮತ್ತು IREE ಏಕೆ ವೇಗವಾಗಿವೆ: ಡಬಲ್-ಅರೇ ಟ್ರೈ
SentencePiece ಮತ್ತು IREE ಎರಡೂ 1989 ರಲ್ಲಿ Aoe ರವಾನಿಸಿದ ಡಬಲ್-ಅರೇ ಟ್ರೈ ಅನ್ನು ಬಳಸುತ್ತವೆ (Aoe, 1989; Karoonboonyanan). ಟ್ರೈ ಅನ್ನು ಎರಡು ಫ್ಲಾಟ್ ಇಂಟರ್ಜರ್ ಅರೇಗಳಲ್ಲಿ ಎಕೋಡ್ ಮಾಡಲಾಗಿದೆ, base ಮತ್ತು check:
int base[N]; // base[node] = offset for this node's children
int check[N]; // check[child] == node validates the parent-child linkಮಕ್ಕಳ ಲುಕ್ಅಪ್ ಹೀಗಿದೆ:
int next = base[node] + byte;
if (check[next] == node) { /* valid transition; child node = next */ }ಟ್ರೈ ಟ್ರಾವರ್ಸಲ್ನ ಪ್ರತಿ ಹಂತಕ್ಕೆ ಕೇವಲ ಎರಡು ಅರೇ ಲೋಡ್ಗಳು, ಒಂದು ಇಂಟರ್ಜರ್ ಸೇರ್ಪಡೆ ಮತ್ತು ಒಂದು ಹೋಲಿಕೆ ಮಾತ್ರ ಬೇಕಾಗುತ್ತದೆ, ಯಾವುದೇ ಹ್ಯಾಶಿಂಗ್, ಯಾವುದೇ ಪಾಯಿಂಟರ್ ಚೇಸಿಂಗ್ ಮತ್ತು ಪ್ರತಿ-ನೋಡ್ ಹೀಪ್ ಹಂಚಿಕೆ ಇಲ್ಲ. ನಿರ್ಮಾಣವು ಪ್ರತಿ ನೋಡ್ ಅನ್ನು ಇರಿಸುತ್ತದೆ ಇದರಿಂದ ಅದರ ಮಕ್ಕಳು ಮೌಲ್ಯಮಾಪನ ಅರೇಯಲ್ಲಿ ಮುಕ್ತ ಸ್ಲಾಟ್ಗಳಲ್ಲಿ ಬೀಳುತ್ತವೆ, ಬಹು ಪೋಷಕರು ತಮ್ಮ ಮಕ್ಕಳು ಅನುಸರಿಸುವ ಬೈಟ್ನಲ್ಲಿ ಭಿನ್ನವಾಗಿರುವವರೆಗೆ ಪ್ರದೇಶಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಅನುವು ಮಾಡಿಕೊಡುತ್ತದೆ.
XLM-RoBERTa ನ 250K ಶಬ್ದಕೋಶಕ್ಕಾಗಿ, ಸಂಪೂರ್ಣ ಟ್ರೈ ~9 MB ನಿರಂತರ ಮೆಮೊರಿಯಲ್ಲಿ ಹೊಂದಿಕೊಳ್ಳುತ್ತದೆ (ಪ್ರತಿ ಸ್ಲಾಟ್ಗೆ ಆಫ್ಸೆಟ್, ಮೌಲ್ಯಮಾಪನ ಮತ್ತು ಟೋಕನ್ ID ಗಾಗಿ ~780K ಸ್ಲಾಟ್ಗಳು × 12 ಬೈಟ್ಗಳು). ಒಂದೇ ಎಕೋಡ್ನ ಹಾಟ್ ವರ್ಕಿಂಗ್ ಸೆಟ್ 100 KB ಕ್ರಮದಲ್ಲಿದೆ, L2 ನಲ್ಲಿ ಆರಾಮವಾಗಿ ಇರುತ್ತದೆ, ಮತ್ತು ಹಾರ್ಡ್ವೇರ್ ಪ್ರಿಫೆಚರ್ ನಿಯಮಿತ ಪ್ರವೇಶ ಮಾದರಿಯಲ್ಲಿ ಲಾಕ್ ಆಗುತ್ತದೆ.
ಇದಕ್ಕೆ ವಿರುದ್ಧವಾಗಿ, ಉಲ್ಲೇಖವು ಪ್ರತಿ ನೋಡ್ಗೆ ಮಕ್ಕಳ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಅನ್ನು ಇರಿಸುತ್ತದೆ, ಪ್ರತಿ ಬೈಟ್ ಹಂತಕ್ಕೆ ಸುಮಾರು ನಾಲ್ಕು ಅವಲಂಬಿತ ಲೋಡ್ಗಳ ವೆಚ್ಚವಾಗುತ್ತದೆ. ಬೈಟ್ ಅನ್ನು ಹ್ಯಾಶ್ ಮಾಡಿ, ಬಕೆಟ್ ಅನ್ನು ಲೋಡ್ ಮಾಡಿ, ಬಕೆಟ್ ನಮೂನೆಗೆ ಪಾಯಿಂಟರ್ ಅನ್ನು ಅನುಸರಿಸಿ, ಹೀಪ್ನಲ್ಲಿರುವ ಮಗುವಿನ ನೋಡ್ಗೆ ಮತ್ತೊಂದು ಪಾಯಿಂಟರ್ ಅನ್ನು ಅನುಸರಿಸಿ, ನಂತರ ಮುಂದಿನ ಬೈಟ್ಗಾಗಿ ಅದರ ಸ್ವಂತ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಅನ್ನು ಸಂಪರ್ಕಿಸಿ. ಹೀಪ್ ಆಬ್ಜೆಕ್ಟ್ಗಳು ಚದುರಿಹೋಗುತ್ತವೆ ಮತ್ತು ಹೆಚ್ಚಿನ ಲೋಡ್ಗಳು ಕ್ಯಾಶ್ ಅನ್ನು ಕಳೆದುಕೊಳ್ಳುತ್ತವೆ.
SP ಮತ್ತು IREE ಎರಡೂ ಟೋಕನ್ ID ಅನ್ನು ನೇರವಾಗಿ ಟ್ರೈ ನೋಡ್ನಲ್ಲಿ ಸಂಗ್ರಹಿಸುತ್ತವೆ. ಉಲ್ಲೇಖ ಟ್ರೈ ಅನುಷ್ಠಾನವು ಹೊಂದಾಣಿಕೆಯಾದ ಬೈಟ್ಗಳನ್ನು ಮಾತ್ರ ಹಿಂತಿರುಗಿಸುತ್ತದೆ, ಆದ್ದರಿಂದ ಎಕೋಡರ್ ಪ್ರತಿ ಹೊಂದಾಣಿಕೆಗೆ ಹೊಸ ಸ್ಟ್ರಿಂಗ್ ಅನ್ನು ಹಂಚಿಕೆ ಮಾಡಬೇಕು, ಟೋಕನ್ ID ಅನ್ನು ಮರುಪಡೆಯಲು ಸೈಡ್ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ನಲ್ಲಿ ಅದನ್ನು ಲುಕ್ ಅಪ್ ಮಾಡಬೇಕು ಮತ್ತು ಪ್ರತ್ಯೇಕ ವೆಕ್ಟರ್ನಿಂದ ಸ್ಕೋರ್ ಅನ್ನು ಓದಬೇಕು. SP ID ಅನ್ನು ಟ್ರೈನ ಸಂಗ್ರಹಿಸಿದ ಮೌಲ್ಯವಾಗಿ ಹಿಂತಿರುಗಿಸುತ್ತದೆ ಮತ್ತು ಫ್ಲಾಟ್ ಫ್ಲೋಟ್ ಅರೆಯಿಂದ ಸ್ಕೋರ್ ಅನ್ನು ಓದುತ್ತದೆ:
// unigram_model.cc:567
const size_t num_nodes = trie_->commonPrefixSearch(
begin, trie_results.data(), trie_results.size(),
static_cast<int>(end - begin));
for (size_t k = 0; k < num_nodes; ++k) {
const int id = trie_results[k].value; // already in the trie
const float score = GetScoreInlined(id); // flat float[] indexed by id
Lattice::Node *node = lattice->Insert(begin_pos, length);
node->id = id;
node->score = score;
}IREE ಒಂದೇ ಕರೆಯಲ್ಲಿ ಎಲ್ಲಾ ಹೊಂದಾಣಿಕೆಗಳನ್ನು ಹಿಂತಿರುಗಿಸುವ ಬದಲು ಸ್ಟೇಟ್ಫುಲ್ ಕರ್ಸರ್ API ಅನ್ನು ಬಳಸುತ್ತದೆ, ಆದರೆ ವೆಚ್ಚದ ಮಾದರಿಯು ಒಂದೇ ಆಗಿರುತ್ತದೆ: ಟೋಕನ್ ID ಗಳು ಮತ್ತು ಸ್ಕೋರ್ಗಳು ಫ್ಲಾಟ್-ಅರೇ ಇಂಡೆಕ್ಸ್ಗಳಾಗಿ ತಲುಪಬಹುದಾದವು, ಟ್ರೈ ವಾಕ್ನಲ್ಲಿ ಯಾವುದೇ ಹಂಚಿಕೆ ಅಥವಾ ಹ್ಯಾಶಿಂಗ್ ಇಲ್ಲ. IREE ಪ್ರತಿ ಎಕೋಡ್ಗೆ ಕೇವಲ ಒಂದು ಔಟ್ಪುಟ್ ಬಫರ್ ಅನ್ನು ಹಂಚಿಕೆ ಮಾಡುತ್ತದೆ; SP ಹೆಚ್ಚುವರಿಯಾಗಿ DP ಸಮಯದಲ್ಲಿ ಪ್ರತಿ ಹೊಂದಾಣಿಕೆಗೆ ಲ್ಯಾಟಿಕ್ಸ್ ನೋಡ್ ಅನ್ನು ಹಂಚಿಕೆ ಮಾಡುತ್ತದೆ, 514 ಟೋಕನ್ಗಳಲ್ಲಿ ಸುಮಾರು 1,559 ಹಂಚಿಕೆಗಳನ್ನು ತಲುಪುತ್ತದೆ.
ಬೇಸ್ಲೈನ್: ಉಲ್ಲೇಖದ ಶೂನ್ಯ-ಹಂಚಿಕೆ ಪೋರ್ಟ್
ಟ್ರೈ ಅನ್ನು ಬದಲಾಯಿಸುವ ಮೊದಲು, ನಾವು ಅಲ್ಗಾರಿದಮ್ ವೆಚ್ಚವನ್ನು ಅನುಷ್ಠಾನ ವೆಚ್ಚದಿಂದ ಪ್ರತ್ಯೇಕಿಸುತ್ತೇವೆ: ಅನಗತ್ಯ ಕೆಲಸವನ್ನು ತೆಗೆದುಹಾಕಿದೊಂದಿಗೆ ಉಲ್ಲೇಖವು ಎಷ್ಟು ವೇಗವಾಗಿ ಚಲಿಸುತ್ತದೆ?
baseline referenceನ hash-map-children trie ಅನ್ನು ಉಳಿಸಿಕೊಳ್ಳುತ್ತದೆ, ಆದರೆ ಎರಡು ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುತ್ತದೆ. caller-owned scratch struct DP table, ಪೂರ್ವಸಂಸ್ಕರಿಸಿದ bytes, ಮತ್ತು output token IDಗಳನ್ನು ಹೊಂದಿರುತ್ತದೆ, ಆದ್ದರಿಂದ hot path warmup ನಂತರ ಯಾವುದೇ allocations ಮಾಡುವುದಿಲ್ಲ. token ID ಮತ್ತು score ಅನ್ನು trie nodeನಲ್ಲೇ ನೇರವಾಗಿ ಸಂಗ್ರಹಿಸಲಾಗುತ್ತದೆ, ಇದರಿಂದ ಪ್ರತಿ ಹೊಂದಾಣಿಕೆಯ string conversion ಮತ್ತು ಪಕ್ಕದ hash-map lookup ತೆಗೆದುಹಾಕಲಾಗುತ್ತದೆ.
ಒಳಗಿನ ಲೂಪ್:
for pos in 0..n {
if scratch.prep[pos] & 0xC0 == 0x80 { continue; }
let s0 = scratch.best_score[pos];
if s0 == f32::NEG_INFINITY { continue; }
let mut node = 0u32;
for end in pos..n {
let byte = scratch.prep[end];
match self.nodes[node as usize].children.get(&byte) {
Some(&child) => {
node = child;
let n = &self.nodes[node as usize];
if n.is_terminal {
let total = s0 + n.score;
if total > scratch.best_score[end + 1] {
scratch.best_score[end + 1] = total;
scratch.best_start[end + 1] = pos as u32;
scratch.best_id[end + 1] = n.token_id;
}
}
}
None => break,
}
}
}ಇದು ಒಂದೇ ಅಲ್ಗಾರಿದಮ್ ಆಗಿದೆ, ಅದೇ ಟ್ರೈ ಆಕಾರ ಮತ್ತು ಅದೇ ಪ್ರವೇಶ ಮಾದರಿಯೊಂದಿಗೆ, ಕೇವಲ ಯಾವುದೇ ಹಂಚಿಕೆಗಳು ಮತ್ತು ಅನಗತ್ಯ ಲುಕ್ಅಪ್ಗಳು ಇಲ್ಲದೆ. 514 ಟೋಕನ್ಗಳಲ್ಲಿ ಫಲಿತಾಂಶಗಳು:
ಎಂಜಿನ್ | p50 | ಸೂಚನೆಗಳು | IPC | ಹಂಚಿಕೆಗಳು |
Hugging Face | 326 µs | 3.66M | 2.86 | 7,295 |
SentencePiece | 127 µs | 1.81M | 3.55 | 1,559 |
IREE | 111 µs | 1.58M | 3.26 | 1 |
ಬೇಸ್ಲೈನ್ | 156 µs | 1.53M | 2.62 | 0 |
514 ಟೋಕನ್ಗಳಲ್ಲಿ ಇರುವ ಹಾಲಿ ಟೋಕನೈಜರ್ ಅನುಷ್ಠಾನಗಳೊಂದಿಗೆ ಹೋಲಿಸಿದ ಶೂನ್ಯ-ಅಲೊಕೇಷನ್ ಮೂಲಾಧಾರ.
ಪ್ರತಿ-ಹೊಂದಾಣಿಕೆ ಸ್ಟ್ರಿಂಗ್ ಭೌತೀಕರಣ ಮತ್ತು ದ್ವಿತೀಯ ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಲುಕ್ಅಪ್ ಅನ್ನು ತೆಗೆದುಹಾಕುವುದು ಉಲ್ಲೇಖಕ್ಕೆ ಹೋಲಿಸಿದರೆ ಸೂಚನೆಗಳನ್ನು 2.4× ಕಡಿಮೆ ಮಾಡುತ್ತದೆ. ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಪಾಯಿಂಟರ್ ಚೇಸ್, ಹಿಂದೆ ಅಲೋಕೇಟರ್ ಕೆಲಸದ ಹಿಂದೆ ಮರೆಮಾಡಲ್ಪಟ್ಟಿತ್ತು, ಈಗ ಪ್ರತಿ-ಬೈಟ್ ವೆಚ್ಚವಾಗಿದೆ ಆದ್ದರಿಂದ IPC ಅವುಗಳೊಂದಿಗೆ ಇಳಿಯುತ್ತದೆ (2.86 → 2.62).
ಬೇಸ್ಲೈನ್ ಪ್ರತಿ ಗಾತ್ರದಲ್ಲಿ ಉಲ್ಲೇಖಕ್ಕೆ ಹೋಲಿಸಿದರೆ p50 ಸುಪ್ತತೆಯನ್ನು 2-3× ಕಡಿಮೆ ಮಾಡುತ್ತದೆ, ಮತ್ತು ಅದರ L2 ಮಿಸ್ ದರವು ಉಲ್ಲೇಖದ 8-48% ಗೆ ಹೋಲಿಸಿದರೆ 2k ಟೋಕನ್ಗಳ ಮೂಲಕ 2-3% ಹತ್ತಿರ ಇರುತ್ತದೆ. ಉಳಿದ ವೆಚ್ಚವೆಂದರೆ HashMap ಪಾಯಿಂಟರ್ ಚೇಸ್, ಇದನ್ನು ಮುಂದಿನ ವಿಭಾಗವು ತಿಳಿಸುತ್ತದೆ.
ಆಪ್ಟಿಮೈಸೇಶನ್ 1: ಹೆಚ್ಚುವರಿ ಅಮೂರ್ತತೆಗಳಿಲ್ಲದ ಡಬಲ್-ಅರೇ ಟ್ರೈ
ಹ್ಯಾಶ್-ಮ್ಯಾಪ್ ಟ್ರೈ ಒಂದು ಪಾಯಿಂಟರ್ ಚೇಸ್ ಆಗಿದೆ. SP ಮತ್ತು IREE ಇದನ್ನು ಡಬಲ್-ಅರೇ ಟ್ರೈ ಮೂಲಕ ಪರಿಹರಿಸುತ್ತವೆ ( ಹಿಂದಿನ ವಿಭಾಗವನ್ನು ನೋಡಿ), ಮತ್ತು ನಾವು ಅದೇ ಡೇಟಾ ರಚನೆಯನ್ನು ಎರವಲು ಪಡೆಯುತ್ತೇವೆ. ವ್ಯತ್ಯಾಸವೆಂದರೆ SP ಮತ್ತು IREE ಸ್ಟ್ರೀಮಿಂಗ್ ಇನ್ಪುಟ್, ಔಟ್ಪುಟ್ ಆಫ್ಸೆಟ್ಗಳು ಮತ್ತು ಬಹು ಎಕೋಡಿಂಗ್ ಮೋಡ್ಗಳನ್ನು ಬೆಂಬಲಿಸುವ ಸಾಮಾನ್ಯ-ಉದ್ದೇಶದ ಲೈಬ್ರರಿಗಳಾಗಿವೆ; ಆ ನಮ್ಯತೆಯು ಟ್ರೈ ಸುತ್ತಲೂ ಹೆಚ್ಚುವರಿ ಬುಕ್ಕೀಪಿಂಗ್ ಅನ್ನು ವೆಚ್ಚ ಮಾಡುತ್ತದೆ (SP ನಲ್ಲಿ ಲ್ಯಾಟಿಕ್ಸ್, IREE ನಲ್ಲಿ ಎಂಟು-ಹಂತದ ಪೈಪ್ಲೈನ್). ನಮಗೆ ಅದರಲ್ಲಿ ಯಾವುದೂ ಅಗತ್ಯವಿಲ್ಲ, ಆದ್ದರಿಂದ ನಾವು ಟ್ರೈ ಅನ್ನು ನೇರವಾಗಿ ವಿಟೆರ್ಬಿ ಲೂಪ್ನಲ್ಲಿ ಇನ್ಲೈನ್ ಮಾಡುತ್ತೇವೆ ಮತ್ತು ಉಳಿದವುಗಳನ್ನು ಕೈಬಿಡುತ್ತೇವೆ.
fn child(&self, node: u32, byte: u8) -> Option<u32> {
let base = self.base[node as usize];
let next = base + byte as i32;
if self.check[next as usize] == node as i32 {
Some(next as u32)
} else {
None
}
}ಒಂದು ಬೈಟ್ ಹಂತಕ್ಕೆ ಎರಡು ಅರೇ ಓದುವಿಕೆಗಳು, ಇಂಟರ್ಜರ್ ಸೇರ್ಪಡೆ ಮತ್ತು ಹೋಲಿಕೆ ವೆಚ್ಚವಾಗುತ್ತದೆ. ಸಂಪೂರ್ಣ ಟ್ರೈ ಸುಮಾರು 9 MB ಆಗಿದೆ, ಆದರೆ ಯಾವುದೇ ಒಂದೇ ಎಕೋಡ್ ಅದರ ~100 KB ಅನ್ನು ಮಾತ್ರ ನಡೆಯುತ್ತದೆ, ಆದ್ದರಿಂದ ಹಾಟ್ ಡೇಟಾ L2 ನಲ್ಲಿ ಇರುತ್ತದೆ.
ಎಂಜಿನ್ | p50 | p99 | ಸೂಚನೆಗಳು | L2 ಮಿಸ್% | L3 ಮಿಸ್% |
Hugging Face | 327 µs | 3,340 µs | 3.66M | 8.17% | 14.66% |
SentencePiece | 127 µs | 1,140 µs | 1.82M | 4.74% | 4.07% |
IREE | 111 µs | 132 µs | 1.61M | 5.82% | 3.87% |
ಬೇಸ್ಲೈನ್ | 155 µs | 1,170 µs | 1.53M | 2.59% | 16.66% |
+ Darts | 68 µs | 80 µs | 1.10M | 6.45% | 11.42% |
ಡಬಲ್-ಅರೆ ಟ್ರೈ ಅಳವಡಿಸಿಕೊಂಡ ನಂತರ ಟೋಕನೈಜರ್ ವಿಳಂಬ, ಸೂಚನೆಗಳು, ಮತ್ತು ಕ್ಯಾಶ್ ಮಿಸ್ ದರಗಳು.
ವಾಲ್-ಕ್ಲಾಕ್ ಬೇಸ್ಲೈನ್ನಿಂದ 2.3× ಮತ್ತು ಉಲ್ಲೇಖದಿಂದ 4.8× ಇಳಿಯುತ್ತದೆ. ಸೂಚನೆಗಳು ಬೇಸ್ಲೈನ್ನಿಂದ ಮತ್ತೊಂದು 28% ಇಳಿಯುತ್ತವೆ. ಲ್ಯಾಟಿಕ್ಸ್ ಮತ್ತು ಪೈಪ್ಲೈನ್ ಅನ್ನು ಕಟ್ಟುವುದು ಅದೇ ಟ್ರೈ ಆಕಾರದಲ್ಲಿ SP ನ 1.82M ಮತ್ತು IREE ನ 1.61M ವಿರುದ್ಧ ಪ್ರತಿ ಎಕೋಡ್ಗೆ 1.10M ಸೂಚನೆಗಳನ್ನು ನಿವೃತ್ತಿಗೊಳಿಸಲು ನಮಗೆ ಅನುಮತಿಸುತ್ತದೆ; ನಮಗೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಾಮಾನ್ಯ-ಉದ್ದೇಶದ API ಗಳಿಗಾಗಿ ಅವು ಇರಿಸಿಕೊಳ್ಳುವ ಬುಕ್ಕೀಪಿಂಗ್ ವ್ಯತ್ಯಾಸವಾಗಿದೆ.
ಆಪ್ಟಿಮೈಸೇಶನ್ 2: ಬಿಟ್ಮ್ಯಾಪ್ ಮತ್ತು ಇನ್ಲೈನ್ ಪ್ಯಾಕಿಂಗ್
ಆಪ್ಟಿಮೈಸೇಶನ್ 1 ರ Darts ಟ್ರೈಗೆ ಪ್ರತಿ ಬೈಟ್ ಹಂತಕ್ಕೆ ಎರಡು ಅವಲಂಬಿತ ಲೋಡ್ಗಳು ಬೇಕಾಗುತ್ತವೆ. ಮೊದಲು ಪೋಷಕರ ಬೇಸ್ ಆಫ್ಸೆಟ್, ನಂತರ ಪೋಷಕರು ಅದನ್ನು ಹೊಂದಿದ್ದಾರೆಂದು ಖಚಿತಪಡಿಸಲು ಅಭ್ಯರ್ಥಿ ಮಗುವಿನ ಸ್ಲಾಟ್ನಲ್ಲಿ ಚೆಕ್ ನಮೂನೆ. ಚೆಕ್ ನಮೂನೆಯು ಮಾಲೀಕತ್ವವನ್ನು ಮಾತ್ರ ಮೌಲ್ಯಮಾಪಕಗೊಳಿಸುತ್ತದೆ. ಯಾವ ಬೈಟ್ಗಳು ಮಕ್ಕಳನ್ನು ಹೊಂದಿವೆ ಎಂಬುದನ್ನು ದಾಖಲಿಸುವ ಪ್ರತಿ-ನೋಡ್ ಬಿಟ್ಮ್ಯಾಪ್ ಮೂಲಕ ಅದೇ ಪ್ರಶ್ನೆಗೆ ಉತ್ತರಿಸಬಹುದು. ಡಬಲ್-ಅರೇ ಟ್ರೈನ ನಿರ್ಮಾಣದಿಂದ, ಪೋಷಕರ ಬಿಟ್ಮ್ಯಾಪ್ನಲ್ಲಿ ಬೈಟ್ b ಗಾಗಿ ಬಿಟ್ ಅನ್ನು ನಿಖರವಾಗಿ ಹೊಂದಿಸಲಾಗುತ್ತದೆ ಯಾವಾಗ ಆ ಬೈಟ್ಗಾಗಿ ಅಭ್ಯರ್ಥಿ ಸ್ಲಾಟ್ ಅನ್ನು ಪೋಷಕರು ಹೊಂದಿದ್ದಾರೆ. ಬಿಟ್ಮ್ಯಾಪ್ ಮತ್ತು ಚೆಕ್ ಅರೇ ಒಂದೇ ಮಾಹಿತಿಯನ್ನು ಸಾಗಿಸುತ್ತವೆ, ಆದ್ದರಿಂದ ನಾವು ರನ್ಟೈಮ್ ರಚನೆಯಿಂದ ಚೆಕ್ ಅರೇ ಅನ್ನು ಸಂಪೂರ್ಣವಾಗಿ ಕೈಬಿಡಬಹುದು.
ಬಿಟ್ಮ್ಯಾಪ್ ಲುಕ್ಅಪ್ ಒಂದು 64-ಬಿಟ್ ಪದದ ವಿರುದ್ಧ ಒಂದೇ ಬಿಟ್ ಪರೀಕ್ಷೆಗೆ ಸಂಕಲಿಸುತ್ತದೆ, ಇದು ಸುಮಾರು ಒಂದು ಚಕ್ರದಲ್ಲಿ ಚಲಿಸುತ್ತದೆ. ಸಂಬಂಧಿತ POPCNT ಸೂಚನೆ, ಇದು ರಿಜಿಸ್ಟರ್ನಲ್ಲಿನ ಎಲ್ಲಾ ಸೆಟ್ ಬಿಟ್ಗಳನ್ನು ಎಣಿಸುತ್ತದೆ, ಬಿಟ್ಮ್ಯಾಪ್ನಿಂದ ನೇರವಾಗಿ ಮಗುವಿನ ಆಫ್ಸೆಟ್ ಅನ್ನು ಲೆಕ್ಕಹಾಕಲು ಇತರ ಬಿಟ್ಮ್ಯಾಪ್-ಇಂಡೆಕ್ಸ್ಡ್ ಟ್ರೈ ವಿನ್ಯಾಸಗಳು ಬಳಸುವುದಾಗಿದೆ. ನಮಗೆ ಇದು ಅಗತ್ಯವಿಲ್ಲ ಏಕೆಂದರೆ Darts ನಿರ್ಮಾಣವು ಈಗಾಗಲೇ ನೋಡ್ನ ಬೇಸ್ ಕ್ಷೇತ್ರದಲ್ಲಿ ಆಫ್ಸೆಟ್ ಅನ್ನು ಸಂಗ್ರಹಿಸುತ್ತದೆ.
#[repr(C, align(64))]
struct NodeHot {
bitmap: [u64; 4], // 32 B — "which bytes have children"
base: i32, // 4 B — child slot offset
token_id: i32, // 4 B — vocab id (-1 if not terminal)
score: f64, // 8 B — Viterbi log-prob
_pad: [u8; 16], // 16 B — pad to one cache line
}
// Lookup: one cache-line load, one bit test, one add.
fn child(&self, parent: u32, byte: u8) -> Option<u32> {
let h = &self.node_hot[parent as usize];
if !h.bitmap.test(byte) { return None; }
Some(h.base.wrapping_add(byte as i32) as u32)
}ಅದೇ ವೇಳೆ, ನಾವು ಪ್ರತಿ nodeನ ನಾಲ್ಕು ಕ್ಷೇತ್ರಗಳನ್ನು (bitmap, base, token ID, ಮತ್ತು score) ಒಂದೇ 64-byte cache lineನಲ್ಲಿ ಪ್ಯಾಕ್ ಮಾಡುತ್ತೇವೆ. ಈಗ ಒಂದೇ node ಭೇಟಿ ಮಾಡಿದರೆ ಬೇಕಾದ ಎಲ್ಲವೂ ಒಳಗೆ ಬರುತ್ತದೆ: terminal nodeಗಳಲ್ಲಿ ನಮಗೆ ಬೇಕಾದ score ಮತ್ತು token ID, ಜೊತೆಗೆ ಮುಂದಿನ ಹಂತಕ್ಕೆ bitmap ಮತ್ತು base. check array ಅನ್ನು ನಿರ್ಮಾಣದ ವೇಳೆ ಮಾತ್ರ ಬಳಸಲಾಗುತ್ತದೆ ಮತ್ತು runtime layoutನಿಂದ ತೆಗೆದುಹಾಕಲಾಗುತ್ತದೆ. hot-path indexing ಪರಿಶೀಲನೆಯಿಲ್ಲದೆ ನಡೆಸಲಾಗುತ್ತದೆ, ಏಕೆಂದರೆ trie ನಿರ್ಮಾಣ ಮತ್ತು loop ರಚನೆಯೇ boundsಗಳನ್ನು ಈಗಾಗಲೇ ಖಚಿತಪಡಿಸುತ್ತವೆ. ವಿನಿಮಯವೆಂದರೆ space: ಪ್ರತಿ nodeಗೆ 64 bytes × ~780K slots ಇರುವುದರಿಂದ trie ~9 MB ರಿಂದ ~50 MB ಗೆ ಬೆಳೆಯುತ್ತದೆ.
514 ಟೋಕನ್ಗಳಲ್ಲಿ ಫಲಿತಾಂಶಗಳು:
ಎಂಜಿನ್ | p50 | p99 | ಸೂಚನೆಗಳು | L2 ಪ್ರವೇಶಗಳು | L2 ಮಿಸ್% | L3 ಮಿಸ್% |
Hugging Face | 327 µs | 3,340 µs | 3.66M | 15.0K | 8.17% | 14.66% |
SentencePiece | 127 µs | 1,140 µs | 1.82M | 7.5K | 4.74% | 4.07% |
IREE | 111 µs | 132 µs | 1.61M | 4.7K | 5.82% | 3.87% |
ಬೇಸ್ಲೈನ್ | 155 µs | 1,170 µs | 1.53M | 6.1K | 2.59% | 16.66% |
+ Darts | 68 µs | 80 µs | 1.10M | 4.6K | 6.45% | 11.42% |
+ ಬಿಟ್ಮ್ಯಾಪ್ ಮತ್ತು ಇನ್ಲೈನ್ ಪ್ಯಾಕಿಂಗ್ | 65 µs | 77 µs | 1.06M | 1.8K | 20.53% | 15.01% |
ಬಿಟ್ಮ್ಯಾಪ್ ಮತ್ತು ಇನ್ಲೈನ್ ಪ್ಯಾಕಿಂಗ್ ಸೇರಿಸಿದ ನಂತರ 514 ಟೋಕನ್ಗಳಲ್ಲಿ ಟೋಕನೈಜರ್ ಫಲಿತಾಂಶಗಳು.
ವಾಲ್-ಕ್ಲಾಕ್ ವ್ಯವಸ್ಥಿತವಾಗಿ 514 ಟೋಕನ್ಗಳಲ್ಲಿ ಮತ್ತೊಂದು 4.5% ಇಳಿಯುತ್ತದೆ ಮತ್ತು ಇನ್ಪುಟ್ ಉದ್ದದೊಂದಿಗೆ ಬೆಳೆಯುತ್ತದೆ, 4k ಮತ್ತು 16k ನಲ್ಲಿ 6-7% ತಲುಪುತ್ತದೆ. ಚೆಕ್ ಅರೇ ಅನ್ನು ಕೈಬಿಡುವುದರಿಂದ ಪ್ರತಿ-ಬೈಟ್ ಟ್ರೈ ಹಂತವನ್ನು ಎರಡು ಕ್ಯಾಶ್-ಲೈನ್ ಲೋಡ್ಗಳಿಂದ ಒಂದಕ್ಕೆ ತಿರುಗಿಸುತ್ತದೆ, ಇದು ಪ್ರತಿ ಎಕೋಡ್ಗೆ 2.5× ಕಡಿಮೆ L2 ಪ್ರವೇಶಗಳಾಗಿವೆ (ಸರಳ Darts ನ 4.6K ಗೆ ಹೋಲಿಸಿದರೆ 1.8K). ಮಿಸ್ ದರವು 6% ರಿಂದ 20% ಕ್ಕೆ ಜಿಗಿಯುತ್ತದೆ, ಆದರೆ ಅದರಲ್ಲಿ ಹೆಚ್ಚಿನವು ಛೇದವಾಗಿದೆ: ಅರ್ಧಕ್ಕಿಂತ ಕಡಿಮೆ ಪ್ರವೇಶಗಳೊಂದಿಗೆ, ನಾವು ಹೊಂದಿರುವ ಮಿಸ್ಗಳು ದೊಡ್ಡ ಪಾಲನ್ನು ತೆಗೆದುಕೊಳ್ಳುತ್ತವೆ.
ಆಪ್ಟಿಮೈಸೇಶನ್ 3: ಟ್ರೈಗಾಗಿ ದೊಡ್ಡ ಪುಟಗಳು
ಟ್ರೈ ಡೇಟಾ ಸುಮಾರು 50 MB ಆಗಿದೆ (ಪ್ರತಿಯೊಂದೂ 64 ಬೈಟ್ಗಳಲ್ಲಿ 780,000 ನೋಡ್ಗಳು). ಡೀಫಾಲ್ಟ್ ಲಿನಕ್ಸ್ ಪ್ರಕ್ರಿಯೆಯಲ್ಲಿ, ಆ ಮೆಮೊರಿಯು 4 KB ಪುಟಗಳಿಂದ ಬೆಂಬಲಿತವಾಗಿದೆ, ಆದ್ದರಿಂದ ಸಂಪೂರ್ಣ ಟ್ರೈ ಸುಮಾರು 12,000 ವಿಭಿನ್ನ ವರ್ಚುವಲ್ ಪುಟಗಳನ್ನು ವ್ಯಾಪಿಸುತ್ತದೆ, CPU ನ ಮೊದಲ-ಹಂತದ ಡೇಟಾ ಟ್ರಾನ್ಸ್ಲೇಷನ್ ಲುಸಡೈಲ್ ಬಫರ್ ಹಿಡಿದಿಟ್ಟುಕೊಳ್ಳುವುದಕ್ಕಿಂತ ಬಹಳ ದೂರದಲ್ಲಿದೆ (ಸಫೈರ್ ರಾಪಿಡ್ಸ್ 96 ನಮೂನೆಗಳನ್ನು ಹೊಂದಿದೆ). ಪ್ರತಿ ವಿಟೆರ್ಬಿ ಹಂತವು ವಿಭಿನ್ನ ನೋಡ್ ಅನ್ನು ಸ್ಪರ್ಶಿಸುತ್ತದೆ, ಮತ್ತು 512-ಟೋಕನ್ ಎಕೋಡ್ ಮೇಲೆ ನಾವು ಪುಟ-ಕೋಷ್ಟಕ ನಡಿಗೆಗಳಲ್ಲಿ ಸುಮಾರು 9,000 ಚಕ್ರಗಳನ್ನು ಸುಡುತ್ತೇವೆ, ಪ್ರತಿ-ಎಕೋಡ್ ಬಜೆಟ್ನ ಸುಮಾರು 3%.
4 KB ಬದಲಿಗೆ 2 MB ಹ್ಯೂಜ್ ಪುಟಗಳೊಂದಿಗೆ ಟ್ರೈ ಅನ್ನು ಬೆಂಬಲಿಸುವುದು ಪರಿಹಾರವಾಗಿದೆ. ಅದೇ 50 MB ಈಗ 25 ಪುಟಗಳನ್ನು ವ್ಯಾಪಿಸುತ್ತದೆ, ಟ್ರಾನ್ಸ್ಲೇಷನ್ ಲುಸಡೈಲ್ ಬಫರ್ನಲ್ಲಿ ಸಂಪೂರ್ಣವಾಗಿ ಹೊಂದಿಕೊಳ್ಳುತ್ತದೆ, ಮತ್ತು ಪುಟ-ಕೋಷ್ಟಕ ಡೈರೆಕ್ಟರಿಗಳು L2 ಕ್ಯಾಶ್ಗಾಗಿ ಟ್ರೈ ಜೊತೆ ಸ್ಪರ್ಧಿಸುವುದನ್ನು ನಿಲ್ಲಿಸುತ್ತವೆ.
Linuxಗೆ 2 MB pages ಮೂಲಕ memory ಅನ್ನು back ಮಾಡಲು ಎರಡು ಮಾರ್ಗಗಳಿವೆ. ಸಂದರ್ಭಾವಕಾಶಾಧಾರಿತ ಮಾರ್ಗವು transparent huge pages (THP): ನಿಮ್ಮ ಪ್ರೋಗ್ರಾಂ kernelಗೆ memory ಪ್ರದೇಶವನ್ನು 4 KB ರಿಂದ 2 MB pagesಗೆ upgrade ಮಾಡಲು ಕೇಳುತ್ತದೆ, ಮತ್ತು ಹಿನ್ನೆಲೆಯಲ್ಲಿ ನಡೆಯುವ kernel threadಗೆ ಸಮಯ ಹಾಗೂ ನಿರಂತರ physical memory ಲಭ್ಯವಿರುವಾಗ ಅದನ್ನು ಮಾಡಲು ಪ್ರಯತ್ನಿಸುತ್ತದೆ. ಇನ್ನೊಂದು ಪರ್ಯಾಯವೆಂದರೆ boot ಸಮಯದಲ್ಲೇ kernel ಪಕ್ಕಕ್ಕೆ ಇಡುವ ಪೂರ್ವ-ರಿಸರ್ವ್ huge-page poolನಿಂದಲೇ trie ಅನ್ನು ನೇರವಾಗಿ allocate ಮಾಡುವುದು:
let raw = unsafe {
libc::mmap(
std::ptr::null_mut(),
alloc_len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_HUGETLB,
-1,
0,
)
};ಪೂಲ್ ಹೋಸ್ಟ್ನಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿರಬೇಕು (ಬೂಟ್ನಲ್ಲಿ vm.nr_hugepages ಹೊಂದಿಸಲಾಗಿದೆ). ನಮ್ಮ ಉತ್ಪಾದನಾ ನಿದರ್ಶನಗಳಲ್ಲಿ, 10,561 ಹ್ಯೂಜ್ ಪುಟಗಳನ್ನು ಕಾಯ್ದಿರಿಸಲಾಗಿದೆ; ಟ್ರೈ ಅವುಗಳಲ್ಲಿ 24 ಅನ್ನು ಬಳಸುತ್ತದೆ. mmap ನಂತರ, /proc/<pid>/smaps ಪ್ರದೇಶವನ್ನು ದೃಢೀಕರಿಸುತ್ತದೆ: KernelPageSize: 2048 kB, Private_Hugetlb: 49152 kB.
ಗಳಿಕೆಯು ಇನ್ಪುಟ್ ಗಾತ್ರಗಳಾದ್ಯಂತ 3–12% ವಾಲ್-ಕ್ಲಾಕ್ ಆಗಿದೆ, ಕೆಲಸ ಮಾಡುವ ಸೆಟ್ ಉದ್ದಕ್ಕೆ ಸ್ಥಿರ ಮತ್ತು ಅನುಪಾತದಲ್ಲಿದೆ:
ಟೋಕನ್ಗಳು | ಹ್ಯೂಜ್ ಪುಟಗಳಿಲ್ಲದೆ | ಹ್ಯೂಜ್ ಪುಟಗಳೊಂದಿಗೆ | Δ |
130 | 15.7 µs | 15.2 µs | −3.6% |
514 | 65.4 µs | 63.1 µs | −3.4% |
1,026 | 129.5 µs | 125.3 µs | −3.3% |
4,098 | 773 µs | 679 µs | −12.0% |
16,386 | 3,808 µs | 3,554 µs | −6.8% |
ಇನ್ಪುಟ್ ಗಾತ್ರಗಳಾದ್ಯಂತ ದೊಡ್ಡ ಪುಟಗಳಿರುವ ಮತ್ತು ಇಲ್ಲದ ಟೋಕನೈಜರ್ ವಿಳಂಬ.
ಅತಿ ದೊಡ್ಡ ಗೆಲುವು 4k ಟೋಕನ್ಗಳಲ್ಲಿದೆ, ಅಲ್ಲಿ ಕೆಲಸ ಮಾಡುವ ಸೆಟ್ ಎಷ್ಟು ದೊಡ್ಡದಾಗಿದೆ ಎಂದರೆ ಪುಟ-ಕೋಷ್ಟಕ ಟ್ರಾಫಿಕ್ L2 ಬ್ಯಾಂಡ್ವಿಡ್ತ್ಗಾಗಿ ಟ್ರೈ ಡೇಟಾದೊಂದಿಗೆ ಸಕ್ರಿಯವಾಗಿ ಸ್ಪರ್ಧಿಸುತ್ತಿತ್ತು. 4k ಮೀರಿ ಗಳಿಕೆಯು ಕುಗ್ಗುತ್ತದೆ ಏಕೆಂದರೆ L3 ಮಿಸ್ಗಳು ಪ್ರಾಬಲ್ಯ ಸಾಧಿಸುತ್ತವೆ ಮತ್ತು ಪುಟ-ಕೋಷ್ಟಕ ನಡಿಗೆಗಳು ಒಟ್ಟು ಸಮಯದ ಸಣ್ಣ ಪಾಲಾಗುತ್ತವೆ.
ಅಂತಿಮ ಹೋಲಿಕೆ
ಮೂರು ಆಪ್ಟಿಮೈಸೇಶನ್ಗಳನ್ನು ಒಟ್ಟಿಗೆ ಎಳೆತಂದರೆ, ನಾವು ಪರೀಕ್ಷಿಸಿದ ಪ್ರತಿಯೊಂದು ಇತರ RoBERTa ಟೋಕನೈಸರ್ ವಿರುದ್ಧ ಅಳೆದ ಅಂತಿಮ ಎಂಜಿನ್ ಇಲ್ಲಿದೆ. ಸ್ಥಳೀಯ C++ SentencePiece ಮತ್ತು C IREE ಬೈನರಿಗಳ ಜೊತೆಗೆ, ಉತ್ಪಾದನಾ ಬಳಕೆದಾರರು ತಲುಪುವ ಆಫ್-ದಿ-ಶೆಲ್ಫ್ ರಸ್ಟ್ ರಾಪರ್ ಕ್ರೇಟ್ಗಳಾದ sentencepiece ಮತ್ತು iree-tokenizer ಅನ್ನು ಸಹ ನಾವು ಬೆಂಚ್ಮಾರ್ಕ್ ಮಾಡಿದ್ದೇವೆ. sentencepiece ಕ್ರೇಟ್ ಪ್ರತಿ ಇನ್ಪುಟ್ ಗಾತ್ರದಲ್ಲಿ ಸ್ಥಳೀಯ C++ ಬೈನರಿಗಿಂತ 1.6-1.9× ಹೆಚ್ಚಿನ ಸುಪ್ತತೆಯನ್ನು ಅಳೆಯುತ್ತದೆ, ಪ್ರತಿ ಎಕೋಡ್ನಲ್ಲಿ ಪ್ರತಿ ತುಣುಕು ಪಠ್ಯದ ತನ್ನದೇ ಆದ ನಕಲನ್ನು ಹೊಂದಿರುವ ಟೋಕನ್ ತುಣುಕುಗಳ ತಾಜಾ ಪಟ್ಟಿಯನ್ನು ರಾಪರ್ ಹಂಚಿಕೆ ಮಾಡುವುದರಿಂದ ಓವರ್ಹೆಡ್ ಬರುತ್ತದೆ. iree-tokenizer ಕ್ರೇಟ್ ತನ್ನ ಆಂತರಿಕ ಬಫರ್ ಅನ್ನು ಪ್ರತಿ ಇನ್ಪುಟ್ಗೆ ಗಾತ್ರಗೊಳಿಸುತ್ತದೆ, ಇದು ಇನ್ಪುಟ್ ಗಾತ್ರವನ್ನು ಲೆಕ್ಕಿಸದೆ ಸ್ಥಳೀಯ IREE ಬೈನರಿ ಪಾವತಿಸುವ ಸ್ಥಿರ ಪ್ರತಿ-ಎಕೋಡ್ ಹಂಚಿಕೆಯನ್ನು ತಪ್ಪಿಸುತ್ತದೆ, ಮತ್ತು ಹಂಚಿಕೆಯು ನೈಜ ಟೋಕನೈಸೇಷನ್ ಕೆಲಸದಲ್ಲಿ ಪ್ರಾಬಲ್ಯ ಸಾಧಿಸುವ ಚಿಕ್ಕ ಇನ್ಪುಟ್ಗಳಲ್ಲಿ ಇದು ಹೆಚ್ಚು ಮುಖ್ಯವಾಗಿದೆ. ಉತ್ಪಾದನಾ ಇನ್ಪುಟ್ ಉದ್ದಗಳಲ್ಲಿ ಸ್ಥಿರ ವೆಚ್ಚವು ಅಮೋರ್ಟೈಸ್ ಆಗುತ್ತದೆ ಮತ್ತು ಟೋಕನ್ಗಳನ್ನು ಮತ್ತೆ ರಸ್ಟ್ಗೆ ನಕಲು ಮಾಡುವುದರಿಂದ ರಾಪರ್ನ ಸ್ವಂತ ಪ್ರತಿ-ಕರೆ ಓವರ್ಹೆಡ್ ಸ್ಥಳೀಯ ಬೈನರಿಯ ಮೇಲೆ ಅದರ ಸುಪ್ತತೆಯನ್ನು ತಳ್ಳುತ್ತದೆ.
ಅಂತಿಮ ಎಂಜಿನ್ ಅದೇ 250,000-ಟೋಕನ್ ಶಬ್ದಕೋಶದ ಮೇಲೆ ಅದೇ ವಿಟೆರ್ಬಿಯನ್ನು ಚಲಾಯಿಸುತ್ತದೆ, ಹಾಟ್ ಪಾತ್ನಲ್ಲಿ ಯಾವುದನ್ನೂ ಹಂಚಿಕೆ ಮಾಡುವುದಿಲ್ಲ, ಮತ್ತು ಉಲ್ಲೇಖದ ವಿರುದ್ಧ ಟೋಕನ್-ನಿಖರವಾದ ಔಟ್ಪುಟ್ ಅನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ. ಪ್ರತಿ ಬೈಟ್ ಹಂತವು ಈಗ ಒಂದೇ 64-ಬೈಟ್ ಕ್ಯಾಶ್-ಲೈನ್ ಲೋಡ್ ಆಗಿದೆ, ಒಂದು ಬಿಟ್ಮ್ಯಾಪ್ ಪರೀಕ್ಷೆ, ಮತ್ತು ಒಂದು ಇಂಟರ್ಜರ್ ಸೇರ್ಪಡೆ, HashMap ಪ್ರೋಬ್, ಅಲೋಕೇಟರ್ ಕರೆ, ಮತ್ತು ಬೇಸ್ಲೈನ್ ಅನುಷ್ಠಾನದಲ್ಲಿ ಹಲವಾರು ವಿಶೇಷ-ಕರೆ ಶಾಖೆಗಳಿಂದ ಕೆಳಗೆ. ಪ್ರತಿ ಎಕೋಡ್ಗೆ ಸೂಚನೆಗಳು 3.66M ನಿಂದ 1.04M ಗೆ ಇಳಿದವು, 3.5× ಇಳಿಕೆ. ವಾಲ್-ಕ್ಲಾಕ್ ಸಣ್ಣ ಇನ್ಪುಟ್ಗಳಲ್ಲಿ ಆ ಅನುಪಾತಕ್ಕೆ ಹೊಂದಿಕೆಯಾಗುತ್ತದೆ ಮತ್ತು ದೀರ್ಘ ಇನ್ಪುಟ್ಗಳಲ್ಲಿ ಮತ್ತಷ್ಟು ಅಗಲವಾಗುತ್ತದೆ, ಅಲ್ಲಿ ಉಲ್ಲೇಖದ ಪ್ರತಿ-ಟೋಕನ್ ಹಂಚಿಕೆಗಳು L2 ಮತ್ತು L3 ಅನ್ನು ಓವರ್ಫ್ಲೋ ಮಾಡಲು ಪ್ರಾರಂಭಿಸುತ್ತವೆ.
ಮುಕ್ತಾಯದ ಆಲೋಚನೆಗಳು
ಅಭ್ಯಾಸದಲ್ಲಿ ಈ ಕೆಲಸವು ನಮ್ಮ ಇನ್ಫರೆನ್ಸ್ ಸ್ಟ್ಯಾಕ್ನ ಪೂರ್ವಭಾವಿ ಸಂಸ್ಕರಣಾ ಪದರದಲ್ಲಿ CPU ಬಳಕೆಯನ್ನು 5-6× ಕಡಿಮೆ ಮಾಡಿದೆ ಮತ್ತು ರಿರಾಂಕರ್ ಸುಪ್ತತೆಯಲ್ಲಿ ದಶಕಗಳ ಮಿಲಿಸೆಕೆಂಡ್ಗಳನ್ನು ಕಡಿಮೆ ಮಾಡಿದೆ.
ಸುಪ್ತತೆ ಕಡಿತದ ಬಹುಪಾಲು ವ್ಯರ್ಥ ಕೆಲಸವನ್ನು ತೆಗೆದುಹಾಕಿದ ಹಗುರವಾದ ಬದಲಾವಣೆಗಳಿಂದ ಬಂದಿದೆ: ಪ್ರತಿ-ಎಕೋಡ್ ಹಂಚಿಕೆಗಳನ್ನು ಕೈಬಿಡುವುದು, HashMap-ಮಕ್ಕಳ ಟ್ರೈ ಅನ್ನು ಡಬಲ್-ಅರೇಯೊಂದಿಗೆ ಬದಲಾಯಿಸುವುದು, ಮತ್ತು ಜೆನೆರಿಕ್-API ಓವರ್ಹೆಡ್ ಅನ್ನು ತೆಗೆದುಹಾಕುವುದು. ಅಸ್ತಿತ್ವದಲ್ಲಿರುವ ಪ್ರತಿ-ನೋಡ್ ಬಿಟ್ಮ್ಯಾಪ್ ಅನ್ನು ಮಾನ್ಯತೆ ಪರಿಶೀಲನೆಯಾಗಿ ಬಳಸುವುದು ಮತ್ತು 2 MB ಹ್ಯೂಜ್ ಪುಟಗಳೊಂದಿಗೆ ಟ್ರೈ ಅನ್ನು ಬೆಂಬಲಿಸುವುದು ಇನ್ಪುಟ್ ಗಾತ್ರವನ್ನು ಅವಲಂಬಿಸಿ ಡಬಲ್-ಅರೇ ಟ್ರೈಗೆ ದೊಡ್ಡ ಬದಲಾವಣೆಯ ಮೇಲ್ಭಾಗದಲ್ಲಿ 10-20% ರಷ್ಟು ಖಾತೆ ಮಾಡಿದೆ.
SentencePiece ಮತ್ತು IREE ಸುತ್ತಲಿನ ಆಫ್-ದಿ-ಶೆಲ್ಫ್ ರಸ್ಟ್ ರಾಪರ್ ಕ್ರೇಟ್ಗಳು ಅದೇ ಬಿಂದುವನ್ನು ಹಿಮ್ಮುಖವಾಗಿ ವಿವರಿಸುತ್ತವೆ. ಸ್ಥಳೀಯ C ಮತ್ತು C++ ಟೋಕನೈಸರ್ಗಳ ಮೇಲೆ, FFI ಮಿತಿ ಮತ್ತು ರಾಪರ್ಗಳು ಪರಿಚಯಿಸುವ ಪ್ರತಿ-ಕರೆ ಹಂಚಿಕೆಗಳು ಸುಪ್ತತೆ ಸಂಖ್ಯೆಗಳಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ. ಅಂತಿಮ ವಿಟೆರ್ಬಿ ಎಕೋಡರ್ಗಾಗಿ ರಸ್ಟ್ ಮೂಲವು github.com/perplexityai/pplx-garden ನಲ್ಲಿ ಲಭ್ಯವಿದೆ.
ಉಲ್ಲೇಖಗಳು
Hugging Face tokenizers — https://github.com/huggingface/tokenizers
SentencePiece — https://github.com/google/sentencepiece
IREE tokenizer — https://github.com/iree-org/iree
Kudo, T. (2018). Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates. https://arxiv.org/abs/1804.10959
Viterbi, A. J. (1967). Error bounds for convolutional codes and an asymptotically optimum decoding algorithm. IEEE Transactions on Information Theory. https://en.wikipedia.org/wiki/Viterbi_algorithm
Aoe, J. (1989). An efficient digital search algorithm by using a double-array structure. IEEE Transactions on Software Engineering. https://ieeexplore.ieee.org/document/31365/
Karoonboonyanan, T. An Implementation of Double-Array Trie. https://linux.thai.net/~thep/datrie/datrie.html
POPCNT (x86) — https://www.felixcloutier.com/x86/popcnt
Huge pages — https://docs.kernel.org/admin-guide/mm/hugetlbpage.html
ಉಲ್ಲೇಖ
@online{perplexity2026ImprovingUnigramTokenizerCpuPerformance,
author = {{Perplexity} and Vladimir Zaytsev and Bex Brooker and Daisy Lee and Roshan Shankar},
title = {{Improving unigram tokenizer CPU performance}},
date = {2026-05-20},
url = {https://www.perplexity.ai/hub/blog/improving-unigram-tokenizer-cpu-performance}
}