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 | import fs from 'node:fs'; | 1 | import fs from 'node:fs'; |
| 2 | import path from 'node:path'; | 2 | import path from 'node:path'; |
| 3 | import { Buffer } from 'node:buffer'; | 3 | import { Buffer } from 'node:buffer'; |
| 4 | import zlib from 'node:zlib'; | ||
| 5 | import { promisify } from 'node:util'; | ||
| 4 | 6 | ||
| 5 | import express from 'express'; | 7 | import express from 'express'; |
| 6 | import fetch from 'node-fetch'; | 8 | import fetch from 'node-fetch'; |
| @@ -57,11 +59,12 @@ export const TEXT_COMPLETION_MODELS = [ | |||
| 57 | 59 | ||
| 58 | const CHARS_PER_TOKEN = 3.35; | 60 | const CHARS_PER_TOKEN = 3.35; |
| 59 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); | 61 | const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean'); |
| 62 | const gunzip = promisify(zlib.gunzip); | ||
| 60 | 63 | ||
| 61 | /** | 64 | /** |
| 62 | * Gets a path to the tokenizer model. Downloads the model if it's a URL. | 65 | * Gets a path to the tokenizer model. Downloads the model if it's a URL. |
| 63 | * @param {string} model Model URL or path | 66 | * @param {string} model Model URL or path |
| 64 | * @param {string|undefined} fallbackModel Fallback model path\ | 67 | * @param {string|undefined} fallbackModel Fallback model path |
| 65 | * @returns {Promise<string>} Path to the tokenizer model | 68 | * @returns {Promise<string>} Path to the tokenizer model |
| 66 | */ | 69 | */ |
| 67 | async function getPathToTokenizer(model, fallbackModel) { | 70 | async function getPathToTokenizer(model, fallbackModel) { |
| @@ -87,8 +90,24 @@ async function getPathToTokenizer(model, fallbackModel) { | |||
| 87 | fs.mkdirSync(CACHE_PATH, { recursive: true }); | 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 | const cachedFile = path.join(CACHE_PATH, fileName); | 101 | const cachedFile = path.join(CACHE_PATH, fileName); |
| 91 | if (fs.existsSync(cachedFile)) { | 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 | return cachedFile; | 111 | return cachedFile; |
| 93 | } | 112 | } |
| 94 | 113 | ||
| @@ -103,6 +122,12 @@ async function getPathToTokenizer(model, fallbackModel) { | |||
| 103 | } | 122 | } |
| 104 | 123 | ||
| 105 | const arrayBuffer = await response.arrayBuffer(); | 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 | writeFileAtomicSync(cachedFile, Buffer.from(arrayBuffer)); | 131 | writeFileAtomicSync(cachedFile, Buffer.from(arrayBuffer)); |
| 107 | return cachedFile; | 132 | return cachedFile; |
| 108 | } catch (error) { | 133 | } catch (error) { |
| @@ -203,8 +228,8 @@ class WebTokenizer { | |||
| 203 | 228 | ||
| 204 | try { | 229 | try { |
| 205 | const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel); | 230 | const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel); |
| 206 | const arrayBuffer = fs.readFileSync(pathToModel).buffer; | 231 | const fileBuffer = await fs.promises.readFile(pathToModel); |
| 207 | this.#instance = await Tokenizer.fromJSON(arrayBuffer); | 232 | this.#instance = await Tokenizer.fromJSON(fileBuffer); |
| 208 | console.info('Instantiated the tokenizer for', path.parse(pathToModel).name); | 233 | console.info('Instantiated the tokenizer for', path.parse(pathToModel).name); |
| 209 | return this.#instance; | 234 | return this.#instance; |
| 210 | } catch (error) { | 235 | } catch (error) { |
| @@ -223,11 +248,11 @@ const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model'); | |||
| 223 | const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model'); | 248 | const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model'); |
| 224 | const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json'); | 249 | const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json'); |
| 225 | const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json'); | 250 | const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json'); |
| 226 | const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json', 'src/tokenizers/llama3.json'); | 251 | const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json.gz', 'src/tokenizers/llama3.json'); |
| 227 | const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json', 'src/tokenizers/llama3.json'); | 252 | const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json.gz', 'src/tokenizers/llama3.json'); |
| 228 | const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json', 'src/tokenizers/llama3.json'); | 253 | const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json.gz', 'src/tokenizers/llama3.json'); |
| 229 | const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json', 'src/tokenizers/llama3.json'); | 254 | const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json.gz', 'src/tokenizers/llama3.json'); |
| 230 | const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json', 'src/tokenizers/llama3.json'); | 255 | const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json.gz', 'src/tokenizers/llama3.json'); |
| 231 | 256 | ||
| 232 | export const sentencepieceTokenizers = [ | 257 | export const sentencepieceTokenizers = [ |
| 233 | 'llama', | 258 | 'llama', |
| @@ -1092,7 +1117,7 @@ router.post('/remote/textgenerationwebui/encode', async function (request, respo | |||
| 1092 | 1117 | ||
| 1093 | /** @type {any} */ | 1118 | /** @type {any} */ |
| 1094 | const data = await result.json(); | 1119 | const data = await result.json(); |
| 1095 | const count = (data?.length ?? data?.count ?? data?.value ?? data?.tokens?.length); | 1120 | const count = (data?.length ?? data?.count ?? data?.value ?? data?.tokens?.length); |
| 1096 | const ids = (data?.tokens ?? data?.ids ?? []); | 1121 | const ids = (data?.tokens ?? data?.ids ?? []); |
| 1097 | 1122 | ||
| 1098 | return response.send({ count, ids }); | 1123 | return response.send({ count, ids }); |