Support gzip compression for tokenizer downloads (#4348) * Support gzip compression for tokenizer downloads * Use Uint8Array for buffer conversion * Add gunzip support for manually downloaded tokenizer files * Remove underlying buffer access * Remove additional buffer allocations
Signed| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | import fs from 'node:fs'; |
| 2 | 2 | import path from 'node:path'; |
| 3 | 3 | import { Buffer } from 'node:buffer'; |
| 4 | +import zlib from 'node:zlib'; | |
| 5 | +import { promisify } from 'node:util'; | |
| 4 | 6 | |
| 5 | 7 | import express from 'express'; |
| 6 | 8 | import fetch from 'node-fetch'; |
| @@ -57,11 +59,12 @@ export const TEXT_COMPLETION_MODELS = [ | ||
| 57 | 59 | |
| 58 | 60 | const CHARS_PER_TOKEN = 3.35; |
| 59 | 61 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); |
| 62 | +const gunzip = promisify(zlib.gunzip); | |
| 60 | 63 | |
| 61 | 64 | /** |
| 62 | 65 | * Gets a path to the tokenizer model. Downloads the model if it's a URL. |
| 63 | 66 | * @param {string} model Model URL or path |
| 64 | 67 | * @param {string|undefined} fallbackModel Fallback model path\ |
| 65 | 68 | * @returns {Promise<string>} Path to the tokenizer model |
| 66 | 69 | */ |
| 67 | 70 | async function getPathToTokenizer(model, fallbackModel) { |
| @@ -87,8 +90,24 @@ async function getPathToTokenizer(model, fallbackModel) { | ||
| 87 | 90 | fs.mkdirSync(CACHE_PATH, { recursive: true }); |
| 88 | 91 | } |
| 89 | 92 | |
| 93 | + // If an uncompressed version exists, return it | |
| 94 | + const isCompressed = path.extname(fileName) === '.gz'; | |
| 95 | + const uncompressedName = path.basename(fileName, '.gz'); | |
| 96 | + const uncompressedPath = path.join(CACHE_PATH, uncompressedName); | |
| 97 | + if (isCompressed && fs.existsSync(uncompressedPath)) { | |
| 98 | + return uncompressedPath; | |
| 99 | + } | |
| 100 | + | |
| 90 | 101 | const cachedFile = path.join(CACHE_PATH, fileName); |
| 91 | 102 | if (fs.existsSync(cachedFile)) { |
| 103 | + // If the file was downloaded manually | |
| 104 | + if (isCompressed) { | |
| 105 | + const compressedBuffer = await fs.promises.readFile(cachedFile); | |
| 106 | + const decompressedBuffer = await gunzip(compressedBuffer); | |
| 107 | + writeFileAtomicSync(uncompressedPath, decompressedBuffer); | |
| 108 | + await fs.promises.unlink(cachedFile); | |
| 109 | + return uncompressedPath; | |
| 110 | + } | |
| 92 | 111 | return cachedFile; |
| 93 | 112 | } |
| 94 | 113 | |
| @@ -103,6 +122,12 @@ async function getPathToTokenizer(model, fallbackModel) { | ||
| 103 | 122 | } |
| 104 | 123 | |
| 105 | 124 | const arrayBuffer = await response.arrayBuffer(); |
| 125 | + if (isCompressed) { | |
| 126 | + const decompressedBuffer = await gunzip(arrayBuffer); | |
| 127 | + writeFileAtomicSync(uncompressedPath, decompressedBuffer); | |
| 128 | + return uncompressedPath; | |
| 129 | + } | |
| 130 | + | |
| 106 | 131 | writeFileAtomicSync(cachedFile, Buffer.from(arrayBuffer)); |
| 107 | 132 | return cachedFile; |
| 108 | 133 | } catch (error) { |
| @@ -203,8 +228,8 @@ class WebTokenizer { | ||
| 203 | 228 | |
| 204 | 229 | try { |
| 205 | 230 | const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel); |
| 206 | 231 | const arrayBufferfileBuffer = await fs.readFileSyncpromises.readFile(pathToModel).buffer; |
| 207 | 232 | this.#instance = await Tokenizer.fromJSON(arrayBufferfileBuffer); |
| 208 | 233 | console.info('Instantiated the tokenizer for', path.parse(pathToModel).name); |
| 209 | 234 | return this.#instance; |
| 210 | 235 | } catch (error) { |
| @@ -223,11 +248,11 @@ const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model'); | ||
| 223 | 248 | const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model'); |
| 224 | 249 | const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json'); |
| 225 | 250 | const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json'); |
| 226 | 251 | const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json.gz', 'src/tokenizers/llama3.json'); |
| 227 | 252 | const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json.gz', 'src/tokenizers/llama3.json'); |
| 228 | 253 | const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json.gz', 'src/tokenizers/llama3.json'); |
| 229 | 254 | const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json.gz', 'src/tokenizers/llama3.json'); |
| 230 | 255 | const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json.gz', 'src/tokenizers/llama3.json'); |
| 231 | 256 | |
| 232 | 257 | export const sentencepieceTokenizers = [ |
| 233 | 258 | 'llama', |