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

4db44af11062724fe3181ba3c9ba5721da24a48a

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
1 files changed, +33 -8Showing whitespace changes
src/endpoints/tokenizers.js+33 -8
@@ -1,6 +1,8 @@
1import fs from 'node:fs';1import fs from 'node:fs';
2import path from 'node:path';2import path from 'node:path';
3import { Buffer } from 'node:buffer';3import { Buffer } from 'node:buffer';
4import zlib from 'node:zlib';
5import { promisify } from 'node:util';
46
5import express from 'express';7import express from 'express';
6import fetch from 'node-fetch';8import fetch from 'node-fetch';
@@ -57,11 +59,12 @@ export const TEXT_COMPLETION_MODELS = [
5759
58const CHARS_PER_TOKEN = 3.35;60const CHARS_PER_TOKEN = 3.35;
59const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');61const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');
62const gunzip = promisify(zlib.gunzip);
6063
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 path66 * @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 model68 * @returns {Promise<string>} Path to the tokenizer model
66 */69 */
67async function getPathToTokenizer(model, fallbackModel) {70async 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 }
8992
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 }
94113
@@ -103,6 +122,12 @@ async function getPathToTokenizer(model, fallbackModel) {
103 }122 }
104123
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 {
203228
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');
223const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model');248const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model');
224const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');249const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
225const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');250const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');
226const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json', 'src/tokenizers/llama3.json');251const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json.gz', 'src/tokenizers/llama3.json');
227const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json', 'src/tokenizers/llama3.json');252const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json.gz', 'src/tokenizers/llama3.json');
228const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json', 'src/tokenizers/llama3.json');253const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json.gz', 'src/tokenizers/llama3.json');
229const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json', 'src/tokenizers/llama3.json');254const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json.gz', 'src/tokenizers/llama3.json');
230const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json', 'src/tokenizers/llama3.json');255const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json.gz', 'src/tokenizers/llama3.json');
231256
232export const sentencepieceTokenizers = [257export const sentencepieceTokenizers = [
233 'llama',258 'llama',