|
|
import { gguf, type GGUFTypedMetadata, buildGgufHeader } from "@huggingface/gguf"; |
|
|
import { |
|
|
commitIter, |
|
|
downloadFile, |
|
|
type CommitOutput, |
|
|
type CommitProgressEvent, |
|
|
} from "@huggingface/hub"; |
|
|
import * as fs from "fs"; |
|
|
|
|
|
async function fetchMetadata( |
|
|
repoName: string, |
|
|
path: string |
|
|
): Promise<{ |
|
|
typedMetadata: GGUFTypedMetadata; |
|
|
ggufData: { |
|
|
tensorDataOffset: number; |
|
|
littleEndian: boolean; |
|
|
tensorInfoByteRange?: [number, number]; |
|
|
}; |
|
|
}> { |
|
|
const url = `https://huggingface.co/${repoName}/resolve/main/${path}`; |
|
|
const ggufResult = await gguf(url, { |
|
|
typedMetadata: true, |
|
|
additionalFetchHeaders: { |
|
|
Authorization: `Bearer ${process.env.HF_TOKEN}`, |
|
|
}, |
|
|
}); |
|
|
|
|
|
const typedMetadata = ggufResult.typedMetadata; |
|
|
const ggufData = { |
|
|
tensorDataOffset: Number(ggufResult.tensorDataOffset), |
|
|
littleEndian: Boolean(ggufResult.littleEndian ?? true), |
|
|
tensorInfoByteRange: ggufResult.tensorInfoByteRange, |
|
|
}; |
|
|
return { typedMetadata, ggufData }; |
|
|
} |
|
|
|
|
|
async function commitGgufMetadata({ |
|
|
repoName, |
|
|
path, |
|
|
metadata, |
|
|
ggufData, |
|
|
}: { |
|
|
repoName: string; |
|
|
path: string; |
|
|
metadata: GGUFTypedMetadata; |
|
|
ggufData: { |
|
|
tensorDataOffset: number; |
|
|
littleEndian: boolean; |
|
|
tensorInfoByteRange?: [number, number]; |
|
|
}; |
|
|
}): Promise<{ commit: CommitOutput["commit"]; pullRequestUrl?: string } | undefined> { |
|
|
let uploadProgress = 0; |
|
|
let maxUploadProgress = 0; |
|
|
|
|
|
let fileUploadProgress: { |
|
|
fileName: string; |
|
|
fileSizeBytes?: number; |
|
|
progressRatio: number; |
|
|
state: "uploading" | "error" | "completed"; |
|
|
} = { |
|
|
fileName: path, |
|
|
progressRatio: 0, |
|
|
state: "uploading", |
|
|
}; |
|
|
|
|
|
|
|
|
maxUploadProgress = 0; |
|
|
|
|
|
try { |
|
|
|
|
|
const tensorDataOffset = ggufData!.tensorDataOffset; |
|
|
const littleEndian = ggufData!.littleEndian; |
|
|
const tensorInfoByteRange = ggufData!.tensorInfoByteRange; |
|
|
|
|
|
if (!tensorInfoByteRange) { |
|
|
throw new Error("tensorInfoByteRange not found"); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const originalStreamingBlob = await downloadFile({ |
|
|
repo: repoName, |
|
|
path, |
|
|
xet: true, |
|
|
accessToken: process.env.HF_TOKEN, |
|
|
}); |
|
|
|
|
|
if (!originalStreamingBlob) { |
|
|
throw new Error("Failed to get GGUF file reference"); |
|
|
} |
|
|
|
|
|
|
|
|
fileUploadProgress.fileSizeBytes = originalStreamingBlob.size; |
|
|
|
|
|
const alignment = Number(metadata["general.alignment"]?.value ?? 32); |
|
|
|
|
|
const finalHeaderBlob = await buildGgufHeader(originalStreamingBlob, metadata, { |
|
|
littleEndian, |
|
|
tensorInfoByteRange, |
|
|
alignment, |
|
|
}); |
|
|
|
|
|
|
|
|
const commitProgress = commitIter({ |
|
|
repo: repoName, |
|
|
title: "benchmark", |
|
|
isPullRequest: true, |
|
|
accessToken: process.env.HF_TOKEN, |
|
|
|
|
|
useXet: true, |
|
|
operations: [ |
|
|
{ |
|
|
operation: "edit", |
|
|
path, |
|
|
originalContent: originalStreamingBlob, |
|
|
edits: [ |
|
|
{ |
|
|
start: 0, |
|
|
end: Number(tensorDataOffset), |
|
|
content: finalHeaderBlob, |
|
|
}, |
|
|
], |
|
|
}, |
|
|
], |
|
|
}); |
|
|
|
|
|
|
|
|
let result: CommitOutput | undefined; |
|
|
let done = false; |
|
|
let lastLoggedProgress = -1; |
|
|
const progressThreshold = 5; |
|
|
|
|
|
while (!done) { |
|
|
const { value, done: isDone } = await commitProgress.next(); |
|
|
done = isDone ?? false; |
|
|
|
|
|
if (done) { |
|
|
result = value as CommitOutput; |
|
|
} else { |
|
|
|
|
|
const progressEvent = value as CommitProgressEvent; |
|
|
if (progressEvent.event === "fileProgress") { |
|
|
const progressRatio = progressEvent.progress ?? 0; |
|
|
const state = progressEvent.state ?? ""; |
|
|
uploadProgress = progressRatio * 100; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (fileUploadProgress) { |
|
|
if (state === "hashing") { |
|
|
|
|
|
const newProgressRatio = progressRatio * 0.5; |
|
|
if (newProgressRatio > maxUploadProgress) { |
|
|
maxUploadProgress = newProgressRatio; |
|
|
fileUploadProgress.progressRatio = newProgressRatio; |
|
|
} |
|
|
} else if (state === "uploading") { |
|
|
|
|
|
const newProgressRatio = 0.5 + progressRatio * 0.5; |
|
|
if (newProgressRatio > maxUploadProgress) { |
|
|
maxUploadProgress = newProgressRatio; |
|
|
fileUploadProgress.progressRatio = newProgressRatio; |
|
|
} |
|
|
|
|
|
|
|
|
if (progressRatio === 1) { |
|
|
fileUploadProgress.progressRatio = 1; |
|
|
maxUploadProgress = 1; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const progress = Math.round(uploadProgress); |
|
|
|
|
|
if ( |
|
|
progress >= lastLoggedProgress + progressThreshold || |
|
|
(progress === 100 && lastLoggedProgress < 100) |
|
|
) { |
|
|
lastLoggedProgress = progress; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const hubCommit = result?.commit ?? { oid: "", url: "" }; |
|
|
const pullRequestUrl = result?.pullRequestUrl; |
|
|
|
|
|
|
|
|
if (fileUploadProgress) { |
|
|
fileUploadProgress.state = "completed"; |
|
|
fileUploadProgress.progressRatio = 1; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return { commit: hubCommit, pullRequestUrl }; |
|
|
} catch (err) { |
|
|
|
|
|
if (fileUploadProgress && err instanceof Error && err.name !== "AbortError") { |
|
|
fileUploadProgress.state = "error"; |
|
|
} |
|
|
|
|
|
throw err; |
|
|
} |
|
|
} |
|
|
|
|
|
async function getPaths(repoName: string): Promise<string[]> { |
|
|
const res = await fetch(`https://huggingface.co/api/models/${repoName}/tree/main`); |
|
|
const data = await res.json(); |
|
|
const paths = data.map((item: any) => item.path); |
|
|
const ggufPaths = paths.filter((path: string) => path.endsWith(".gguf")); |
|
|
|
|
|
|
|
|
ggufPaths.sort((a: string, b: string) => { |
|
|
const numA = parseInt(a.match(/(\d+)mb\.gguf$/)?.[1] || "0"); |
|
|
const numB = parseInt(b.match(/(\d+)mb\.gguf$/)?.[1] || "0"); |
|
|
return numA - numB; |
|
|
}); |
|
|
|
|
|
return ggufPaths; |
|
|
} |
|
|
|
|
|
(async () => { |
|
|
const repoName = "mishig/xet-gguf-edit-test"; |
|
|
const csvFilePath = "benchmark-results.csv"; |
|
|
|
|
|
|
|
|
fs.writeFileSync(csvFilePath, "path,time_seconds,time_minutes\n"); |
|
|
|
|
|
const paths = await getPaths(repoName); |
|
|
|
|
|
for (const path of paths) { |
|
|
console.log(`\nProcessing: ${path}`); |
|
|
|
|
|
try { |
|
|
const { typedMetadata, ggufData } = await fetchMetadata(repoName, path); |
|
|
const newMetadata = { |
|
|
...typedMetadata, |
|
|
"tokenizer.chat_template": { |
|
|
type: typedMetadata["tokenizer.chat_template"]?.type, |
|
|
value: "You are a helpful assistant.", |
|
|
}, |
|
|
} as GGUFTypedMetadata; |
|
|
|
|
|
const startTime = performance.now(); |
|
|
|
|
|
const result = await commitGgufMetadata({ |
|
|
repoName, |
|
|
path, |
|
|
metadata: newMetadata, |
|
|
ggufData, |
|
|
}); |
|
|
|
|
|
const endTime = performance.now(); |
|
|
const timeInSeconds = ((endTime - startTime) / 1000).toFixed(2); |
|
|
const timeInMinutes = (parseFloat(timeInSeconds) / 60).toFixed(2); |
|
|
|
|
|
|
|
|
fs.appendFileSync(csvFilePath, `${path},${timeInSeconds},${timeInMinutes}\n`); |
|
|
|
|
|
console.log(`✓ ${path} - ${timeInSeconds}s (${timeInMinutes}min)`); |
|
|
console.log(` Commit: ${result?.commit.url || "N/A"}`); |
|
|
} catch (error) { |
|
|
console.error(`✗ ${path} Error:`, error); |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(`\n✓ Benchmark complete! Results saved to ${csvFilePath}`); |
|
|
})(); |
|
|
|