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 @@
11import fs from 'node:fs';
22import path from 'node:path';
33import { Buffer } from 'node:buffer';
4+import zlib from 'node:zlib';
5+import { promisify } from 'node:util';
46
57import express from 'express';
68import fetch from 'node-fetch';
@@ -57,11 +59,12 @@ export const TEXT_COMPLETION_MODELS = [
5759
5860const CHARS_PER_TOKEN = 3.35;
5961const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');
62+const gunzip = promisify(zlib.gunzip);
6063
6164/**
6265 * Gets a path to the tokenizer model. Downloads the model if it's a URL.
6366 * @param {string} model Model URL or path
6467 * @param {string|undefined} fallbackModel Fallback model path\
6568 * @returns {Promise<string>} Path to the tokenizer model
6669 */
6770async function getPathToTokenizer(model, fallbackModel) {
@@ -87,8 +90,24 @@ async function getPathToTokenizer(model, fallbackModel) {
8790 fs.mkdirSync(CACHE_PATH, { recursive: true });
8891 }
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+
90101 const cachedFile = path.join(CACHE_PATH, fileName);
91102 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+ }
92111 return cachedFile;
93112 }
94113
@@ -103,6 +122,12 @@ async function getPathToTokenizer(model, fallbackModel) {
103122 }
104123
105124 const arrayBuffer = await response.arrayBuffer();
125+ if (isCompressed) {
126+ const decompressedBuffer = await gunzip(arrayBuffer);
127+ writeFileAtomicSync(uncompressedPath, decompressedBuffer);
128+ return uncompressedPath;
129+ }
130+
106131 writeFileAtomicSync(cachedFile, Buffer.from(arrayBuffer));
107132 return cachedFile;
108133 } catch (error) {
@@ -203,8 +228,8 @@ class WebTokenizer {
203228
204229 try {
205230 const pathToModel = await getPathToTokenizer(this.#model, this.#fallbackModel);
206231 const arrayBufferfileBuffer = await fs.readFileSyncpromises.readFile(pathToModel).buffer;
207232 this.#instance = await Tokenizer.fromJSON(arrayBufferfileBuffer);
208233 console.info('Instantiated the tokenizer for', path.parse(pathToModel).name);
209234 return this.#instance;
210235 } catch (error) {
@@ -223,11 +248,11 @@ const spp_gemma = new SentencePieceTokenizer('src/tokenizers/gemma.model');
223248const spp_jamba = new SentencePieceTokenizer('src/tokenizers/jamba.model');
224249const claude_tokenizer = new WebTokenizer('src/tokenizers/claude.json');
225250const llama3_tokenizer = new WebTokenizer('src/tokenizers/llama3.json');
226251const commandRTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-r.json.gz', 'src/tokenizers/llama3.json');
227252const commandATokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/command-a.json.gz', 'src/tokenizers/llama3.json');
228253const qwen2Tokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/qwen2.json.gz', 'src/tokenizers/llama3.json');
229254const nemoTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/nemo.json.gz', 'src/tokenizers/llama3.json');
230255const deepseekTokenizer = new WebTokenizer('https://github.com/SillyTavern/SillyTavern-Tokenizers/raw/main/deepseek.json.gz', 'src/tokenizers/llama3.json');
231256
232257export const sentencepieceTokenizers = [
233258 'llama',