File size: 3,219 Bytes
98acb70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
-- bpe.lua - Load lua-llama's BPE tokenizer without loading a model.
--
-- Exact token counts need the real tokenizer, and the real tokenizer is a
-- 433KB file. But repo 1's Tokenizer.new(path, vocab_size) takes the vocab
-- size as an argument, and the only place the rest of this repo had one was
-- model.config.vocab_size -- so counting tokens meant loading a 60MB
-- checkpoint and waiting ~17s for it.
--
-- That coupling is incidental: tokenizer.bin describes its own length, you
-- just have to walk it. This scans the file to derive the entry count, then
-- hands that to repo 1's loader. The replay and http backends get exact
-- counts with no model at all.
--
-- Format (llama2.c):
--   int32  max_token_length
--   repeated: float32 score, int32 len, len bytes

local bpe = {}

-- Walk the file and count entries. ~433KB and a few thousand reads, so it
-- costs milliseconds -- against 17s and 60MB for the checkpoint it replaces.
function bpe.count_entries(path)
    local fh, err = io.open(path, "rb")
    if not fh then return nil, tostring(err) end

    local header = fh:read(4)
    if not header or #header ~= 4 then
        fh:close()
        return nil, "truncated header"
    end

    local n = 0
    while true do
        local score = fh:read(4)
        if not score or #score < 4 then break end
        local len_bytes = fh:read(4)
        if not len_bytes or #len_bytes < 4 then
            fh:close()
            return nil, string.format("truncated length field at entry %d", n)
        end
        local len = string.unpack("<i4", len_bytes)
        if len < 0 or len > 1024 then
            fh:close()
            return nil, string.format("implausible token length %d at entry %d", len, n)
        end
        local tok = fh:read(len)
        if not tok or #tok ~= len then
            fh:close()
            return nil, string.format("truncated token body at entry %d", n)
        end
        n = n + 1
    end
    fh:close()

    if n == 0 then return nil, "no tokens found" end
    return n
end

-- Returns tokenizer, vocab_size  or  nil, err.
-- llama_path is where repo 1 lives, needed because its tokenizer module
-- requires('utils') by bare name.
function bpe.load(path, llama_path)
    local n, err = bpe.count_entries(path)
    if not n then return nil, err end

    local root = llama_path or "../lua_llama"
    package.path = root .. "/?.lua;" .. package.path

    local ok, Tokenizer = pcall(require, "tokenizer")
    if not ok then return nil, "cannot load lua-llama's tokenizer.lua: " .. tostring(Tokenizer) end

    -- Repo 1's loader prints as it goes; useful in its CLI, noise here.
    local real_print = print
    _G.print = function() end
    local loaded, tok = pcall(Tokenizer.new, path, n)
    _G.print = real_print

    if not loaded then return nil, tostring(tok) end
    return tok, n
end

-- The stand-in when no tokenizer file is available: roughly four characters
-- per token. Good enough to exercise the eviction policies, and never
-- reported as exact.
function bpe.approximate()
    return { encode = function(_, s)
        local out = {}
        for k = 1, math.max(1, math.ceil(#s / 4)) do out[k] = k end
        return out
    end }
end

return bpe