mlboydaisuke's picture
Granite-Embedding-97M-Multilingual-R2: fp32 + w8 bundles, S=128/512, macOS JIT + iPhone 17 Pro h18p AOT, tokenizer, reference fixtures, provenance
0807d1b verified
Raw
History Blame Contribute Delete
11.5 kB
import Foundation
/// Independent tokenizer for the pinned Granite 97M R2 raw-text contract.
/// No prompts, trimming, Unicode normalization, HF package, or model execution.
final class GraniteTokenizer {
enum TokenizerError: Error, CustomStringConvertible {
case unsupported(String)
case missingToken(String)
var description: String {
switch self {
case .unsupported(let message): return "Unsupported tokenizer: \(message)"
case .missingToken(let token): return "Missing BPE token: \(token)"
}
}
}
private struct AddedToken: Decodable {
let id: Int32
let content: String
let single_word: Bool
let lstrip: Bool
let rstrip: Bool
let normalized: Bool
}
private struct Model: Decodable {
let type: String
let ignore_merges: Bool
let byte_fallback: Bool
let dropout: Double?
let unk_token: String?
let continuing_subword_prefix: String?
let end_of_word_suffix: String?
let vocab: [String: Int32]
let merges: [[String]]
}
private struct TokenizerData: Decodable {
let model: Model
let added_tokens: [AddedToken]
}
private let vocab: [String: Int32]
private let ranks: [String: Int]
private let pattern: NSRegularExpression
private let addedPattern: NSRegularExpression
private let wordStart: NSRegularExpression
private let wordEnd: NSRegularExpression
private let added: [String: AddedToken]
private let byteAlphabet: [String]
private var cache: [String: [Int32]] = [:]
private let clsID: Int32 = 179934
private let sepID: Int32 = 179938
private let padID: Int32 = 179935
init(tokenizerURL: URL, tokenizerConfigURL: URL) throws {
let bytes = try Data(contentsOf: tokenizerURL)
let typed = try JSONDecoder().decode(TokenizerData.self, from: bytes)
guard let root = try JSONSerialization.jsonObject(with: bytes) as? [String: Any],
root["normalizer"] is NSNull,
let pre = root["pre_tokenizer"] as? [String: Any],
pre["type"] as? String == "Sequence",
let steps = pre["pretokenizers"] as? [[String: Any]], steps.count == 2,
steps[0]["type"] as? String == "Split",
steps[0]["behavior"] as? String == "Isolated",
steps[0]["invert"] as? Bool == false,
let regexPattern = steps[0]["pattern"] as? [String: String],
regexPattern.count == 1, let expression = regexPattern["Regex"],
steps[1]["type"] as? String == "ByteLevel",
steps[1]["add_prefix_space"] as? Bool == false,
steps[1]["use_regex"] as? Bool == false
else { throw TokenizerError.unsupported("normalizer or pretokenizer") }
let model = typed.model
guard model.type == "BPE", model.ignore_merges, !model.byte_fallback,
model.dropout == nil, model.unk_token == nil,
model.continuing_subword_prefix == nil, model.end_of_word_suffix == nil
else { throw TokenizerError.unsupported("BPE configuration") }
guard let post = root["post_processor"] as? [String: Any],
post["type"] as? String == "TemplateProcessing",
let single = post["single"] as? [[String: Any]], single.count == 3,
(single[0]["SpecialToken"] as? [String: Any])?["id"] as? String == "<|startoftext|>",
(single[1]["Sequence"] as? [String: Any])?["id"] as? String == "A",
(single[2]["SpecialToken"] as? [String: Any])?["id"] as? String == "<|return|>"
else { throw TokenizerError.unsupported("single-text template") }
let configBytes = try Data(contentsOf: tokenizerConfigURL)
guard let config = try JSONSerialization.jsonObject(with: configBytes) as? [String: Any],
(config["padding_side"] as? String ?? "right") == "right",
(config["truncation_side"] as? String ?? "right") == "right",
config["cls_token"] as? String == "<|startoftext|>",
config["sep_token"] as? String == "<|return|>",
config["pad_token"] as? String == "<|endoftext|>"
else { throw TokenizerError.unsupported("special tokens/padding/truncation") }
var added: [String: AddedToken] = [:]
for token in typed.added_tokens {
guard !token.lstrip, !token.rstrip, !token.normalized, !token.content.isEmpty
else { throw TokenizerError.unsupported("added-token flags") }
added[token.content] = token
}
guard added["<|startoftext|>"]?.id == 179934,
added["<|return|>"]?.id == 179938,
added["<|endoftext|>"]?.id == 179935
else { throw TokenizerError.unsupported("special IDs") }
self.added = added
self.vocab = model.vocab
var ranks: [String: Int] = [:]
ranks.reserveCapacity(model.merges.count)
for (rank, pair) in model.merges.enumerated() {
guard pair.count == 2, !pair[0].contains("\0"), !pair[1].contains("\0")
else { throw TokenizerError.unsupported("merge pair") }
ranks[pair[0] + "\0" + pair[1]] = rank
}
self.ranks = ranks
self.pattern = try NSRegularExpression(pattern: expression)
let addedExpression = added.keys.sorted {
$0.utf8.count == $1.utf8.count ? $0 < $1 : $0.utf8.count > $1.utf8.count
}.map(NSRegularExpression.escapedPattern(for:)).joined(separator: "|")
self.addedPattern = try NSRegularExpression(pattern: addedExpression)
self.wordStart = try NSRegularExpression(pattern: #"^\w"#)
self.wordEnd = try NSRegularExpression(pattern: #"\w$"#)
let visible = Set(Array(33...126) + Array(161...172) + Array(174...255))
var extra = 0
self.byteAlphabet = (0...255).map { byte in
if visible.contains(byte) { return String(UnicodeScalar(byte)!) }
defer { extra += 1 }
return String(UnicodeScalar(256 + extra)!)
}
}
private func bpe(_ token: String) throws -> [Int32] {
if let existing = cache[token] { return existing }
// ignore_merges gives a vocabulary hit precedence over applying merges.
if let id = vocab[token] { return [id] }
// HF BPE omits absent initial symbols if UNK and byte fallback are both
// unset. This vocabulary lacks some controls, including mapped NUL.
var symbols = token.unicodeScalars.map(String.init).filter { vocab[$0] != nil }
while symbols.count > 1 {
var bestRank = Int.max
var bestIndex: Int?
for index in 0..<(symbols.count - 1) {
if let rank = ranks[symbols[index] + "\0" + symbols[index + 1]], rank < bestRank {
bestRank = rank
bestIndex = index
}
}
guard let index = bestIndex else { break }
symbols[index] += symbols[index + 1]
symbols.remove(at: index + 1)
}
let ids = try symbols.map { symbol -> Int32 in
guard let id = vocab[symbol] else { throw TokenizerError.missingToken(symbol) }
return id
}
if cache.count < 32768 { cache[token] = ids }
return ids
}
private func ordinary(_ text: String) throws -> [Int32] {
let nsText = text as NSString
let full = NSRange(location: 0, length: nsText.length)
var ids: [Int32] = []
var cursor = 0
func appendPiece(_ range: NSRange) throws {
guard range.length > 0 else { return }
let raw = nsText.substring(with: range)
let encoded = raw.utf8.map { byteAlphabet[Int($0)] }.joined()
ids.append(contentsOf: try bpe(encoded))
}
for match in pattern.matches(in: text, range: full) {
if match.range.location > cursor {
try appendPiece(NSRange(location: cursor, length: match.range.location - cursor))
}
try appendPiece(match.range)
cursor = NSMaxRange(match.range)
}
if cursor < nsText.length {
try appendPiece(NSRange(location: cursor, length: nsText.length - cursor))
}
return ids
}
func encodeBody(text: String) throws -> [Int32] {
let nsText = text as NSString
let full = NSRange(location: 0, length: nsText.length)
var ids: [Int32] = []
var cursor = 0
for match in addedPattern.matches(in: text, range: full) {
let content = nsText.substring(with: match.range)
guard let token = added[content] else { throw TokenizerError.missingToken(content) }
if token.single_word {
let before = nsText.substring(to: match.range.location)
let after = nsText.substring(from: NSMaxRange(match.range))
let leftWord = wordEnd.firstMatch(in: before, range: NSRange(location: 0, length: (before as NSString).length)) != nil
let rightWord = wordStart.firstMatch(in: after, range: NSRange(location: 0, length: (after as NSString).length)) != nil
if leftWord || rightWord { continue }
}
ids.append(contentsOf: try ordinary(nsText.substring(with: NSRange(location: cursor, length: match.range.location - cursor))))
ids.append(token.id)
cursor = NSMaxRange(match.range)
}
ids.append(contentsOf: try ordinary(nsText.substring(from: cursor)))
return ids
}
func encode(text: String, sequenceLength: Int) throws -> (inputIDs: [Int32], attentionMask: [Int32]) {
guard sequenceLength >= 2 else { throw TokenizerError.unsupported("sequenceLength must be >=2") }
let body = try encodeBody(text: text)
var ids = [clsID] + Array(body.prefix(sequenceLength - 2)) + [sepID]
var mask = Array(repeating: Int32(1), count: ids.count)
let padding = sequenceLength - ids.count
ids.append(contentsOf: repeatElement(padID, count: padding))
mask.append(contentsOf: repeatElement(Int32(0), count: padding))
return (ids, mask)
}
}
#if GRANITE_TOKENIZER_CLI
@main
enum GraniteTokenizerCLI {
struct Request: Decodable { let id: String; let text: String; let sequence_length: Int }
struct Response: Encodable { let id: String; let input_ids: [Int32]; let attention_mask: [Int32] }
static func main() throws {
guard CommandLine.arguments.count == 4 else {
throw GraniteTokenizer.TokenizerError.unsupported("usage: tokenizer tokenizer.json tokenizer_config.json requests.json")
}
let tokenizer = try GraniteTokenizer(
tokenizerURL: URL(fileURLWithPath: CommandLine.arguments[1]),
tokenizerConfigURL: URL(fileURLWithPath: CommandLine.arguments[2]))
let requests = try JSONDecoder().decode([Request].self, from: Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[3])))
let responses = try requests.map { request -> Response in
let result = try tokenizer.encode(text: request.text, sequenceLength: request.sequence_length)
return Response(id: request.id, input_ids: result.inputIDs, attention_mask: result.attentionMask)
}
FileHandle.standardOutput.write(try JSONEncoder().encode(responses))
}
}
#endif