184 lines
6.1 KiB
JavaScript
184 lines
6.1 KiB
JavaScript
import { jest } from "@jest/globals";
|
|
|
|
// Mock get_file_metadata before importing the module under test
|
|
const mockGetFileMetadata = jest.fn();
|
|
jest.unstable_mockModule("../../src/utils/model_registry/get_file_metadata.js", () => ({
|
|
get_file_metadata: mockGetFileMetadata,
|
|
}));
|
|
|
|
// Import registry to populate MODEL_TYPE_MAPPING (side-effect import)
|
|
await import("../../src/models/registry.js");
|
|
|
|
// Dynamic import after mock setup (required for ESM)
|
|
const { get_available_dtypes } = await import("../../src/utils/model_registry/get_available_dtypes.js");
|
|
|
|
// A minimal config that mimics a BERT-like encoder-only model
|
|
const ENCODER_ONLY_CONFIG = {
|
|
architectures: ["BertModel"],
|
|
model_type: "bert",
|
|
};
|
|
|
|
// A minimal config for a decoder-only (causal LM) model
|
|
const DECODER_ONLY_CONFIG = {
|
|
architectures: ["LlamaForCausalLM"],
|
|
model_type: "llama",
|
|
};
|
|
|
|
// A minimal config for a Seq2Seq model (encoder + decoder)
|
|
const SEQ2SEQ_CONFIG = {
|
|
architectures: ["T5ForConditionalGeneration"],
|
|
model_type: "t5",
|
|
};
|
|
|
|
// A config with an unknown architecture (falls back to EncoderOnly)
|
|
const UNKNOWN_ARCH_CONFIG = {
|
|
architectures: ["SomeUnknownArchitecture"],
|
|
model_type: "unknown_type",
|
|
};
|
|
|
|
/**
|
|
* Helper: given a set of files that "exist", returns a mock implementation
|
|
* for get_file_metadata that resolves { exists: true } for those files.
|
|
* @param {string[]} existingFiles
|
|
*/
|
|
function setupExistingFiles(...existingFiles) {
|
|
mockGetFileMetadata.mockImplementation((_modelId, filename, _options) => {
|
|
return Promise.resolve({
|
|
exists: existingFiles.includes(filename),
|
|
fromCache: false,
|
|
});
|
|
});
|
|
}
|
|
|
|
describe("get_available_dtypes", () => {
|
|
beforeEach(() => {
|
|
mockGetFileMetadata.mockReset();
|
|
});
|
|
|
|
it("should detect fp32 and q4 for an encoder-only model", async () => {
|
|
setupExistingFiles(
|
|
"onnx/model.onnx", // fp32
|
|
"onnx/model_q4.onnx", // q4
|
|
);
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: ENCODER_ONLY_CONFIG });
|
|
|
|
expect(dtypes).toContain("fp32");
|
|
expect(dtypes).toContain("q4");
|
|
expect(dtypes).not.toContain("fp16");
|
|
expect(dtypes).not.toContain("q8");
|
|
expect(dtypes).not.toContain("int8");
|
|
});
|
|
|
|
it("should detect all dtypes when all files exist", async () => {
|
|
setupExistingFiles(
|
|
"onnx/model.onnx", // fp32
|
|
"onnx/model_fp16.onnx", // fp16
|
|
"onnx/model_int8.onnx", // int8
|
|
"onnx/model_uint8.onnx", // uint8
|
|
"onnx/model_quantized.onnx", // q8
|
|
"onnx/model_q4.onnx", // q4
|
|
"onnx/model_q4f16.onnx", // q4f16
|
|
"onnx/model_bnb4.onnx", // bnb4
|
|
);
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: ENCODER_ONLY_CONFIG });
|
|
|
|
expect(dtypes).toEqual(["fp32", "fp16", "int8", "uint8", "q8", "q4", "q4f16", "bnb4"]);
|
|
});
|
|
|
|
it("should return empty array when no ONNX files exist", async () => {
|
|
setupExistingFiles();
|
|
const dtypes = await get_available_dtypes("test/model", { config: ENCODER_ONLY_CONFIG });
|
|
|
|
expect(dtypes).toEqual([]);
|
|
});
|
|
|
|
it("should require all session files for seq2seq models", async () => {
|
|
// Only encoder has q4, decoder does not — q4 should NOT be available
|
|
setupExistingFiles(
|
|
"onnx/encoder_model.onnx", // fp32 encoder
|
|
"onnx/decoder_model_merged.onnx", // fp32 decoder
|
|
"onnx/encoder_model_q4.onnx", // q4 encoder (but no q4 decoder)
|
|
);
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: SEQ2SEQ_CONFIG });
|
|
|
|
expect(dtypes).toContain("fp32");
|
|
expect(dtypes).not.toContain("q4");
|
|
});
|
|
|
|
it("should list dtype only when all session files exist for seq2seq", async () => {
|
|
// Both encoder and decoder have fp32 and q8
|
|
setupExistingFiles("onnx/encoder_model.onnx", "onnx/decoder_model_merged.onnx", "onnx/encoder_model_quantized.onnx", "onnx/decoder_model_merged_quantized.onnx");
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: SEQ2SEQ_CONFIG });
|
|
|
|
expect(dtypes).toContain("fp32");
|
|
expect(dtypes).toContain("q8");
|
|
expect(dtypes).not.toContain("fp16");
|
|
expect(dtypes).not.toContain("q4");
|
|
});
|
|
|
|
it("should handle decoder-only models", async () => {
|
|
setupExistingFiles("onnx/model.onnx", "onnx/model_q4.onnx", "onnx/model_q4f16.onnx");
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: DECODER_ONLY_CONFIG });
|
|
|
|
expect(dtypes).toContain("fp32");
|
|
expect(dtypes).toContain("q4");
|
|
expect(dtypes).toContain("q4f16");
|
|
expect(dtypes).toHaveLength(3);
|
|
});
|
|
|
|
it("should fall back to EncoderOnly for unknown architectures", async () => {
|
|
setupExistingFiles("onnx/model.onnx", "onnx/model_fp16.onnx");
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: UNKNOWN_ARCH_CONFIG });
|
|
|
|
expect(dtypes).toContain("fp32");
|
|
expect(dtypes).toContain("fp16");
|
|
expect(dtypes).toHaveLength(2);
|
|
});
|
|
|
|
it("should support custom model_file_name", async () => {
|
|
setupExistingFiles("onnx/custom_model.onnx", "onnx/custom_model_q4.onnx");
|
|
|
|
const dtypes = await get_available_dtypes("test/model", {
|
|
config: ENCODER_ONLY_CONFIG,
|
|
model_file_name: "custom_model",
|
|
});
|
|
|
|
expect(dtypes).toContain("fp32");
|
|
expect(dtypes).toContain("q4");
|
|
expect(dtypes).not.toContain("fp16");
|
|
});
|
|
|
|
it("should pass revision and cache_dir to get_file_metadata", async () => {
|
|
setupExistingFiles("onnx/model.onnx");
|
|
|
|
await get_available_dtypes("test/model", {
|
|
config: ENCODER_ONLY_CONFIG,
|
|
revision: "v2",
|
|
cache_dir: "/tmp/cache",
|
|
});
|
|
|
|
// Verify that metadata calls received the correct options
|
|
for (const call of mockGetFileMetadata.mock.calls) {
|
|
expect(call[0]).toBe("test/model");
|
|
expect(call[2]).toMatchObject({ revision: "v2", cache_dir: "/tmp/cache" });
|
|
}
|
|
});
|
|
|
|
it("should only return valid dtype strings", async () => {
|
|
setupExistingFiles("onnx/model.onnx", "onnx/model_fp16.onnx");
|
|
|
|
const dtypes = await get_available_dtypes("test/model", { config: ENCODER_ONLY_CONFIG });
|
|
|
|
const validDtypes = ["fp32", "fp16", "int8", "uint8", "q8", "q4", "q4f16", "bnb4"];
|
|
for (const dtype of dtypes) {
|
|
expect(validDtypes).toContain(dtype);
|
|
}
|
|
});
|
|
});
|