File size: 11,524 Bytes
0807d1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
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