mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(model): preserve uncertainty across released GGUF layouts
Separate common row constraints from established storage sizes. Preserve Q8_1 and K-layout uncertainty, plus numeric IDs and ambiguous textual identities for historically shifted IDs16–25. Keep zero-byte facts and identity limitations independently, and propagate producer facts through totals, cancellation and retained views. Pin released source evidence and document the compatibility cost. Fixes #6848
This commit is contained in:
+80
-1
@@ -57,7 +57,8 @@ name, original type ID, dimensions, relative data offset, file offset and derive
|
||||
byte length. Alignment accepts every positive UINT32 multiple of eight, including
|
||||
non-powers of two. Partial Evidence retains explicit alignment once validated;
|
||||
the default alignment becomes known after metadata completes. Cancellation
|
||||
digests or file identity. Metadata keys preserve their spelling and use lowercase
|
||||
retains completed declarations with unknown digests and unverified final file
|
||||
identity. Metadata keys preserve their spelling and use lowercase
|
||||
ASCII alphanumeric components separated by dots, underscores or hyphens;
|
||||
standard numeric components such as `general.base_model.0.name` are preserved.
|
||||
Hyphenated components are retained for compatibility with the published
|
||||
@@ -80,6 +81,84 @@ Nonfinite floating metadata uses `NaN`, `+Infinity` or `-Infinity` with its
|
||||
declared float type; negative zero is `"-0"`. Unknown tensor types retain their numeric IDs and null byte
|
||||
lengths; affected totals and storage validation remain unknown.
|
||||
|
||||
Q8_1 (type ID 9) has two released layouts: 40 bytes per 32 elements in
|
||||
[llama.cpp b1500](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/ggml-quants.h#L49-L55)
|
||||
and 36 bytes in the pinned
|
||||
[modern ggml layout](https://github.com/ggml-org/ggml/blob/da8e2a54b919557b816b1c2beb432ccb766f7ce0/src/ggml-common.h#L242-L253). Both use GGUF v3 and
|
||||
quantization version 2, so those declarations do not select a storage layout.
|
||||
REA retains the known type, shape, parameter count and starting offsets, with
|
||||
`byte_length: null`, `storage_validation: "unknown"`, partial storage/range
|
||||
coverage and an explicit limitation for nonempty Q8_1 tensors. Per-type and
|
||||
overall byte totals remain unknown where affected. Padding, the next tensor's
|
||||
offset and file size do not establish a layout; retained queries preserve the
|
||||
same uncertainty. Empty Q8_1 tensors still have a derived zero-byte length,
|
||||
and the known 32-element row-block constraint remains enforced.
|
||||
|
||||
K-quantization IDs 10–15 also have incompatible released layouts. The
|
||||
[b1500 build option](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/CMakeLists.txt#L88)
|
||||
`LLAMA_QKK_64` selects `GGML_QKK_64`; the same release's default and
|
||||
[b3001](https://github.com/ggml-org/llama.cpp/blob/c429b33beb35f13934a4dfbe0c138d30b45e5d54/ggml-common.h)
|
||||
use 256-element blocks. The
|
||||
[b1500 structures](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/ggml-quants.h#L61-L169)
|
||||
give these source-derived sizes:
|
||||
|
||||
| ID | Stable name | Bytes per 64 elements, K64 | Bytes per 256 elements, K256 | Padded extent for 256 elements, either layout |
|
||||
| --- | ----------- | -------------------------- | ---------------------------- | --------------------------------------------- |
|
||||
| 10 | Q2_K | 24 | 84 | 96 |
|
||||
| 11 | Q3_K | 28 | 110 | 128 |
|
||||
| 12 | Q4_K | 38 | 144 | 160 |
|
||||
| 13 | Q5_K | 46 | 176 | 192 |
|
||||
| 14 | Q6_K | 54 | 210 | 224 |
|
||||
| 15 | Q8_K | 76 | 292 | 320 |
|
||||
|
||||
Both builds write GGUF v3 and quantization version 2. The
|
||||
[quantizer](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/llama.cpp#L7430-L7492)
|
||||
writes `general.file_type` directly from the selected `llama_ftype`, without
|
||||
encoding the K64 build option. `GGML_QNT_VERSION_FACTOR` is defined in the
|
||||
header but is not used by this GGUF serialization path. Global file type also
|
||||
does not identify individual tensor types: the quantizer
|
||||
[retains unselected tensors](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/llama.cpp#L7561-L7586).
|
||||
The [writer pads each payload](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/ggml.c#L19549-L19565)
|
||||
to the default 32-byte alignment; the final column shows why even the same file
|
||||
extents need not distinguish these layouts.
|
||||
REA preserves each K type's name and enforces the common 64-element row
|
||||
multiple, which does not establish a storage block size. Nonempty K tensor
|
||||
lengths and affected totals are now unknown, including common modern files.
|
||||
Empty K tensors still have derived zero-byte storage.
|
||||
|
||||
Integer IDs also shifted each time IQ types were added. These released
|
||||
`ggml.h` snapshots all declare GGUF v3 and quantization version 2:
|
||||
|
||||
| Released snapshot | I8 ID | I16 ID | I32 ID |
|
||||
| --------------------------------------------------------------------------------------------------- | ----- | ------ | ------ |
|
||||
| [b1500](https://github.com/ggml-org/llama.cpp/blob/57ad015dc3011b046ed5a23186c86ea55f987c54/ggml.h) | 16 | 17 | 18 |
|
||||
| [b1803](https://github.com/ggml-org/llama.cpp/blob/36e5a08b203542dca53cca4eaf172c5dc4bbc991/ggml.h) | 17 | 18 | 19 |
|
||||
| [b1871](https://github.com/ggml-org/llama.cpp/blob/bb0c1392479398f9aba86d9ec98db0b95ede6e6d/ggml.h) | 18 | 19 | 20 |
|
||||
| [b2100](https://github.com/ggml-org/llama.cpp/blob/4aa43fab569215a13495a7f1a0f8afc541b16d03/ggml.h) | 19 | 20 | 21 |
|
||||
| [b2172](https://github.com/ggml-org/llama.cpp/blob/bd2d4e393b2b7d2a1b2e201058e26017c9728ead/ggml.h) | 20 | 21 | 22 |
|
||||
| [b2221](https://github.com/ggml-org/llama.cpp/blob/a14679cc30c785e75d38028bae6ec39c6209ddef/ggml.h) | 21 | 22 | 23 |
|
||||
| [b2253](https://github.com/ggml-org/llama.cpp/blob/4c4cb30736582cacb1a164a9d4bc8e17b1014be7/ggml.h) | 22 | 23 | 24 |
|
||||
| [b2275](https://github.com/ggml-org/llama.cpp/blob/a33e6a0d2a66104ea9a906bdbf8a94d050189d91/ggml.h) | 23 | 24 | 25 |
|
||||
| [b2300](https://github.com/ggml-org/llama.cpp/blob/6ea0f010ff6967034528d9e0b8330b9b0f0b7c13/ggml.h) | 24 | 25 | 26 |
|
||||
|
||||
[Upstream PR 6050](https://github.com/ggml-org/llama.cpp/pull/6050) later pinned
|
||||
integer values explicitly. Modern IDs 16–23 mean IQ2_XXS, IQ2_XS, IQ3_XXS,
|
||||
IQ1_S, IQ4_NL, IQ3_S, IQ2_S and IQ4_XS, respectively. Thus every ID 16–25 has
|
||||
multiple released meanings. REA keeps its numeric `type_id`, uses
|
||||
`AMBIGUOUS_<id>` for `type`, and records the released candidate names in a
|
||||
limitation. Nonempty byte lengths are unknown. Empty declarations still derive
|
||||
zero-byte storage, while their textual identity remains ambiguous. Type filters
|
||||
match the reported `AMBIGUOUS_<id>` label. ID 26 remains I32 across these
|
||||
snapshots; later stable entries retain their existing interpretation. This audit
|
||||
does not establish compatibility with every producer fork or future revision.
|
||||
|
||||
The inventory does not select a historical layout from model names, global
|
||||
file types, padding or guessed producer provenance. A source revision together
|
||||
with its build configuration can establish an external layout fact; these
|
||||
headers do not carry that identification, and REA currently has no layout
|
||||
selection input. Authored regression fixtures use the source-derived sizes
|
||||
above; historical executables were not run.
|
||||
|
||||
Safetensors reports the header extent, exact string `__metadata__` values, dtype,
|
||||
shape and declared data ranges. A null `__metadata__` is treated as omitted;
|
||||
unknown fields on tensor entries are ignored within the reader's 32-level JSON
|
||||
|
||||
@@ -15,7 +15,7 @@ export const MODEL_TOOL_CONTRACTS = [
|
||||
...toolContractMetadata("inspect_model_artifact"),
|
||||
kind: "artifact-provider",
|
||||
description:
|
||||
"Inspect local GGUF v2/v3 or safetensors declarations by content, without loading models or executing code. Default hash header reads only the header: full-file SHA-256 and Evidence subject are unknown; artifact.header_sha256 is a scoped prefix commitment, not weight identity. hash full additionally streams all file bytes for SHA-256. Optional view returns a selected metadata/tensor/type page plus model context in this call. Summary retains complete Evidence for inspect_analysis_view kind model-artifact, collection metadata (keys/indices/fields) or tensors (names/types/indices/fields), with stable header-scoped indices and paging. Metadata arrays retain eight-element typed previews; scalar metadata is complete. Tensor counts, shapes, types and offsets are observed; parameter/storage totals are derived from declarations. Unknown GGUF types retain IDs with unknown byte lengths. Tensor payload, model behavior and inference quality are not inspected. One active inspection, 100 MB header ceiling and 32 MiB retained-facts budget; resource refusals are explicit. No network, external files, Python, model runtime, extraction or deserialization of pickle.",
|
||||
"Inspect local GGUF v2/v3 or safetensors declarations by content, without loading models or executing code. Default hash header reads only the header: full-file SHA-256 and Evidence subject are unknown; artifact.header_sha256 is a scoped prefix commitment, not weight identity. hash full additionally streams all file bytes for SHA-256. Optional view returns a selected metadata/tensor/type page plus model context in this call. Summary retains complete Evidence for inspect_analysis_view kind model-artifact, collection metadata (keys/indices/fields) or tensors (names/types/indices/fields), with stable header-scoped indices and paging. Metadata arrays retain eight-element typed previews; scalar metadata is complete. Tensor counts, shapes, numeric type IDs and offsets are observed; textual types and parameter/storage totals use established format facts. Unknown or historically ambiguous GGUF types retain IDs with unknown nonempty byte lengths; ambiguous identities use AMBIGUOUS_<id> labels. Unresolved Q8_1 and K-block layouts also keep nonempty byte lengths unknown. Tensor payload, model behavior and inference quality are not inspected. One active inspection, 100 MB header ceiling and 32 MiB retained-facts budget; resource refusals are explicit. No network, external files, Python, model runtime, extraction or deserialization of pickle.",
|
||||
inputSchema: inspectModelArtifactInputSchema,
|
||||
outputSchema: evidenceResultOf(
|
||||
z.union([
|
||||
|
||||
+31
-9
@@ -46,6 +46,7 @@ export interface GgufProgress {
|
||||
onMetadataComplete(): void;
|
||||
onTensorsStarted(): void;
|
||||
onTensorDeclaration(row: ModelArtifactPartialTensor): void;
|
||||
onLimitation(message: string): void;
|
||||
}
|
||||
|
||||
/** Explicit-endian GGUF v2/v3 metadata and tensor-directory parser. */
|
||||
@@ -249,6 +250,7 @@ export class GgufHeader {
|
||||
tensor_data_offset: number;
|
||||
metadata: ModelMetadata[];
|
||||
tensors: ModelTensor[];
|
||||
limitations: string[];
|
||||
}> {
|
||||
const versionBytes = await this.reader.take(4);
|
||||
const littleVersion = versionBytes.readUInt32LE();
|
||||
@@ -331,6 +333,12 @@ export class GgufHeader {
|
||||
progress?.onTensorsStarted();
|
||||
const tensors: ModelArtifactPartialTensor[] = [];
|
||||
const names = new Set<string>();
|
||||
const limitations = new Set<string>();
|
||||
const note = (message: string): void => {
|
||||
if (limitations.has(message)) return;
|
||||
limitations.add(message);
|
||||
progress?.onLimitation(message);
|
||||
};
|
||||
for (let index = 0; index < tensorCount; index++) {
|
||||
this.reader.guarantee((tensorCount - index) * 20);
|
||||
const start = this.reader.position;
|
||||
@@ -355,17 +363,30 @@ export class GgufHeader {
|
||||
const type = ggmlType(id);
|
||||
let length: number | null = null;
|
||||
if (type !== undefined) {
|
||||
if (BigInt(shape[0] ?? "1") % BigInt(type[1]) !== 0n)
|
||||
const [, rowElementMultiple, blockBytes, limitation] = type;
|
||||
if (BigInt(shape[0] ?? "1") % BigInt(rowElementMultiple) !== 0n)
|
||||
rejectModel(
|
||||
`GGUF tensor ${name} first dimension is not divisible by the quantization block size`,
|
||||
`GGUF tensor ${name} first dimension must be a multiple of ${String(rowElementMultiple)} elements`,
|
||||
);
|
||||
const bytes = (parameters / BigInt(type[1])) * BigInt(type[2]);
|
||||
if (bytes > BigInt(this.reader.size))
|
||||
rejectModel(
|
||||
`GGUF tensor ${name} declared byte length exceeds the file`,
|
||||
);
|
||||
length = Number(bytes);
|
||||
}
|
||||
if (
|
||||
limitation !== undefined &&
|
||||
(parameters !== 0n || limitation.kind === "type-identity")
|
||||
)
|
||||
note(limitation.reason);
|
||||
if (parameters === 0n) length = 0;
|
||||
else if (blockBytes !== null) {
|
||||
const bytes =
|
||||
(parameters / BigInt(rowElementMultiple)) * BigInt(blockBytes);
|
||||
if (bytes > BigInt(this.reader.size))
|
||||
rejectModel(
|
||||
`GGUF tensor ${name} declared byte length exceeds the file`,
|
||||
);
|
||||
length = Number(bytes);
|
||||
}
|
||||
} else
|
||||
note(
|
||||
`Unknown tensor type UNKNOWN_${String(id)} prevents storage-size validation.`,
|
||||
);
|
||||
const tensor = {
|
||||
index,
|
||||
name,
|
||||
@@ -415,6 +436,7 @@ export class GgufHeader {
|
||||
tensor_data_offset,
|
||||
metadata,
|
||||
tensors: completeTensors,
|
||||
limitations: [...limitations],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
modelCapacityRemediation,
|
||||
modelCapacityReportedLimits,
|
||||
} from "./ModelFormat.js";
|
||||
import { safetensorsBits } from "./ModelTensorTypes.js";
|
||||
import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import {
|
||||
@@ -305,6 +304,7 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
partial.coverage.tensors = "partial";
|
||||
},
|
||||
onTensorDeclaration: (row) => partial.tensor_declarations.push(row),
|
||||
onLimitation: (message) => partial.limitations.push(message),
|
||||
})
|
||||
: await readSafetensorsHeader(reader, first);
|
||||
const intervals = parsed.tensors
|
||||
@@ -324,6 +324,9 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
partial.declared_tensor_count = parsed.tensors.length;
|
||||
partial.tensor_declarations = [];
|
||||
partial.tensors = parsed.tensors;
|
||||
partial.limitations = [
|
||||
...new Set([...partial.limitations, ...parsed.limitations]),
|
||||
];
|
||||
partial.coverage = {
|
||||
header: "complete",
|
||||
metadata: "complete",
|
||||
@@ -384,18 +387,6 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
: group.bytes + row.byte_length;
|
||||
groups.set(row.type, group);
|
||||
}
|
||||
const unknownTypes = [
|
||||
...new Set(
|
||||
parsed.tensors
|
||||
.filter(
|
||||
(row) =>
|
||||
row.byte_length === null ||
|
||||
(format === "safetensors" &&
|
||||
safetensorsBits(row.type) === undefined),
|
||||
)
|
||||
.map((row) => row.type),
|
||||
),
|
||||
];
|
||||
const report = {
|
||||
artifact: {
|
||||
path: input.path,
|
||||
@@ -444,11 +435,7 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
"Metadata arrays retain at most eight elements per array; remaining values are omitted, with exact declared counts and source byte ranges.",
|
||||
]
|
||||
: []),
|
||||
...(unknownTypes.length
|
||||
? [
|
||||
`Unknown tensor types prevent storage-size validation: ${unknownTypes.join(", ")}`,
|
||||
]
|
||||
: []),
|
||||
...parsed.limitations,
|
||||
],
|
||||
};
|
||||
if (jsonValueByteLength(report, MODEL_REPORT_BYTES) > MODEL_REPORT_BYTES)
|
||||
|
||||
@@ -1,48 +1,85 @@
|
||||
/** ggml da8e2a54: enum ggml_type and block sizeof assertions in ggml-common.h.
|
||||
* Unknown future IDs retain their numeric identity and an unknown byte length.
|
||||
*/
|
||||
const GGML_TYPES: Readonly<Record<number, readonly [string, number, number]>> =
|
||||
{
|
||||
0: ["F32", 1, 4],
|
||||
1: ["F16", 1, 2],
|
||||
2: ["Q4_0", 32, 18],
|
||||
3: ["Q4_1", 32, 20],
|
||||
6: ["Q5_0", 32, 22],
|
||||
7: ["Q5_1", 32, 24],
|
||||
8: ["Q8_0", 32, 34],
|
||||
9: ["Q8_1", 32, 36],
|
||||
10: ["Q2_K", 256, 84],
|
||||
11: ["Q3_K", 256, 110],
|
||||
12: ["Q4_K", 256, 144],
|
||||
13: ["Q5_K", 256, 176],
|
||||
14: ["Q6_K", 256, 210],
|
||||
15: ["Q8_K", 256, 292],
|
||||
16: ["IQ2_XXS", 256, 66],
|
||||
17: ["IQ2_XS", 256, 74],
|
||||
18: ["IQ3_XXS", 256, 98],
|
||||
19: ["IQ1_S", 256, 50],
|
||||
20: ["IQ4_NL", 32, 18],
|
||||
21: ["IQ3_S", 256, 110],
|
||||
22: ["IQ2_S", 256, 82],
|
||||
23: ["IQ4_XS", 256, 136],
|
||||
24: ["I8", 1, 1],
|
||||
25: ["I16", 1, 2],
|
||||
26: ["I32", 1, 4],
|
||||
27: ["I64", 1, 8],
|
||||
28: ["F64", 1, 8],
|
||||
29: ["IQ1_M", 256, 56],
|
||||
30: ["BF16", 1, 2],
|
||||
34: ["TQ1_0", 256, 54],
|
||||
35: ["TQ2_0", 256, 66],
|
||||
39: ["MXFP4", 32, 17],
|
||||
40: ["NVFP4", 64, 36],
|
||||
41: ["Q1_0", 128, 18],
|
||||
42: ["Q2_0", 64, 18],
|
||||
};
|
||||
type GgmlType = readonly [
|
||||
name: string,
|
||||
rowElementMultiple: number,
|
||||
blockBytes: number | null,
|
||||
limitation?: {
|
||||
readonly kind: "storage" | "type-identity";
|
||||
readonly reason: string;
|
||||
},
|
||||
];
|
||||
|
||||
export const ggmlType = (
|
||||
id: number,
|
||||
): readonly [string, number, number] | undefined => GGML_TYPES[id];
|
||||
const kStorageLimitation = {
|
||||
kind: "storage",
|
||||
reason:
|
||||
"K-quantization types (IDs 10–15) have released 64-element and 256-element block layouts with the same GGUF and quantization versions. The common 64-element row constraint does not establish a storage block size. Nonempty tensor byte lengths and storage validation remain unknown; file extents and general.file_type do not identify the layout.",
|
||||
} as const;
|
||||
|
||||
const ambiguousGgmlType = (id: number, candidates: string): GgmlType => [
|
||||
`AMBIGUOUS_${String(id)}`,
|
||||
1,
|
||||
null,
|
||||
{
|
||||
kind: "type-identity",
|
||||
reason: `GGUF tensor type ID ${String(id)} has released ${candidates} meanings with the same GGUF and quantization versions. Textual identity and nonempty byte lengths remain unknown; file extents and general.file_type do not identify the per-tensor layout.`,
|
||||
},
|
||||
];
|
||||
|
||||
/** Modern names/sizes: ggml da8e2a54 enum and block sizeof assertions.
|
||||
* Historical collisions: llama.cpp b1500/b1803/b1871/b2100/b2172/b2221/
|
||||
* b2253/b2275/b2300. Their GGUF v3 / quantization v2 do not select a layout.
|
||||
* A common row multiple is necessary, but need not be a known storage block.
|
||||
*/
|
||||
const GGML_TYPES: Readonly<Record<number, GgmlType>> = {
|
||||
0: ["F32", 1, 4],
|
||||
1: ["F16", 1, 2],
|
||||
2: ["Q4_0", 32, 18],
|
||||
3: ["Q4_1", 32, 20],
|
||||
6: ["Q5_0", 32, 22],
|
||||
7: ["Q5_1", 32, 24],
|
||||
8: ["Q8_0", 32, 34],
|
||||
// llama.cpp b1500 (57ad015d) uses two float32 scales (40 bytes), while
|
||||
// ggml da8e2a54 uses two float16 scales (36 bytes). Both declare
|
||||
// GGUF version 3 and quantization version 2; neither selects the layout.
|
||||
9: [
|
||||
"Q8_1",
|
||||
32,
|
||||
null,
|
||||
{
|
||||
kind: "storage",
|
||||
reason:
|
||||
"Q8_1 has released 40-byte and 36-byte layouts per 32 elements with the same GGUF and quantization versions. Nonempty tensor byte lengths and storage validation remain unknown; offsets and file padding do not identify the layout.",
|
||||
},
|
||||
],
|
||||
10: ["Q2_K", 64, null, kStorageLimitation],
|
||||
11: ["Q3_K", 64, null, kStorageLimitation],
|
||||
12: ["Q4_K", 64, null, kStorageLimitation],
|
||||
13: ["Q5_K", 64, null, kStorageLimitation],
|
||||
14: ["Q6_K", 64, null, kStorageLimitation],
|
||||
15: ["Q8_K", 64, null, kStorageLimitation],
|
||||
16: ambiguousGgmlType(16, "I8 / IQ2_XXS"),
|
||||
17: ambiguousGgmlType(17, "I16 / I8 / IQ2_XS"),
|
||||
18: ambiguousGgmlType(18, "I32 / I16 / I8 / IQ3_XXS"),
|
||||
19: ambiguousGgmlType(19, "I32 / I16 / I8 / IQ1_S"),
|
||||
20: ambiguousGgmlType(20, "I32 / I16 / I8 / IQ4_NL"),
|
||||
21: ambiguousGgmlType(21, "I32 / I16 / I8 / IQ3_S"),
|
||||
22: ambiguousGgmlType(22, "I32 / I16 / I8 / IQ2_S"),
|
||||
23: ambiguousGgmlType(23, "I32 / I16 / I8 / IQ4_XS"),
|
||||
24: ambiguousGgmlType(24, "I32 / I16 / I8"),
|
||||
25: ambiguousGgmlType(25, "I32 / I16"),
|
||||
26: ["I32", 1, 4],
|
||||
27: ["I64", 1, 8],
|
||||
28: ["F64", 1, 8],
|
||||
29: ["IQ1_M", 256, 56],
|
||||
30: ["BF16", 1, 2],
|
||||
34: ["TQ1_0", 256, 54],
|
||||
35: ["TQ2_0", 256, 66],
|
||||
39: ["MXFP4", 32, 17],
|
||||
40: ["NVFP4", 64, 36],
|
||||
41: ["Q1_0", 128, 18],
|
||||
42: ["Q2_0", 64, 18],
|
||||
};
|
||||
|
||||
export const ggmlType = (id: number): GgmlType | undefined => GGML_TYPES[id];
|
||||
|
||||
/** safetensors e246a256 Dtype::bitsize, including packed sub-byte types. */
|
||||
const SAFETENSORS_BITS: Readonly<Record<string, number>> = {
|
||||
|
||||
@@ -209,6 +209,7 @@ export const readSafetensorsHeader = async (
|
||||
tensor_data_offset: number;
|
||||
metadata: ModelMetadata[];
|
||||
tensors: ModelTensor[];
|
||||
limitations: string[];
|
||||
}> => {
|
||||
const budget = new ModelReportBudget();
|
||||
// Lengths such as 640 (80 02 ...) also begin like a pickle PROTO opcode.
|
||||
@@ -275,6 +276,7 @@ export const readSafetensorsHeader = async (
|
||||
}
|
||||
}
|
||||
const tensors: ModelTensor[] = [];
|
||||
const unknownTypes = new Set<string>();
|
||||
const tensor_data_offset = 8 + size;
|
||||
const names = Object.keys(root)
|
||||
.filter((name) => name !== "__metadata__")
|
||||
@@ -312,6 +314,7 @@ export const readSafetensorsHeader = async (
|
||||
);
|
||||
}
|
||||
const bits = safetensorsBits(type);
|
||||
if (bits === undefined) unknownTypes.add(type);
|
||||
if (bits !== undefined) {
|
||||
const expected = parameters * BigInt(bits);
|
||||
if (expected % 8n !== 0n || expected / 8n !== BigInt(end - start))
|
||||
@@ -359,5 +362,11 @@ export const readSafetensorsHeader = async (
|
||||
tensor_data_offset,
|
||||
metadata,
|
||||
tensors,
|
||||
limitations:
|
||||
unknownTypes.size === 0
|
||||
? []
|
||||
: [
|
||||
`Unknown tensor types prevent storage-size validation: ${[...unknownTypes].join(", ")}`,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile, unlink } from "node:fs/promises";
|
||||
import { readFile, writeFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
@@ -15,7 +15,12 @@ import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
|
||||
import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js";
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { ggufFixture, safetensorsFixture } from "../fixtures/model/model.js";
|
||||
import {
|
||||
ggufFixture,
|
||||
ggufKLayouts,
|
||||
ggufShiftedTypeLayouts,
|
||||
safetensorsFixture,
|
||||
} from "../fixtures/model/model.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
@@ -69,6 +74,363 @@ cliTest(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves unresolved Q8_1 layout through CLI, MCP summaries and retained tensor/type pages",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-model-q8-layout-");
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const blockBytes of [36, 40]) {
|
||||
const path = join(root, `blocks-${String(blockBytes)}.gguf`);
|
||||
const base = ggufFixture();
|
||||
// Released layouts share GGUF v3/type 9/quantization version 2.
|
||||
// Trailing alignment space is not a storage-layout declaration.
|
||||
const fixture = ggufFixture({
|
||||
metadata: [
|
||||
{ key: "general.quantization_version", type: 4, value: base.u32(2) },
|
||||
],
|
||||
tensors: [
|
||||
{ name: "control", type: 0, shape: [4n], offset: 0n, bytes: 16 },
|
||||
{
|
||||
name: "weight",
|
||||
type: 9,
|
||||
shape: [320n],
|
||||
offset: 32n,
|
||||
bytes: 10 * blockBytes,
|
||||
},
|
||||
],
|
||||
});
|
||||
const bytes = Buffer.concat([fixture.bytes, Buffer.alloc(16)]);
|
||||
await writeFile(path, bytes);
|
||||
const cliResult = await cli.run({
|
||||
arguments: [
|
||||
"inspect-model-artifact",
|
||||
path,
|
||||
"--detail",
|
||||
"complete",
|
||||
"--hash",
|
||||
"full",
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(cliResult.exitCode, cliResult.stderr).toBe(0);
|
||||
const report = modelArtifactSchema.parse(
|
||||
parseEvidence(cliResult.json).normalized_result,
|
||||
);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
expect(report.artifact).toMatchObject({ path, sha256 });
|
||||
expect(report.tensors[0]).toMatchObject({
|
||||
byte_length: 16,
|
||||
storage_validation: "consistent",
|
||||
});
|
||||
const weight = {
|
||||
index: 1,
|
||||
name: "weight",
|
||||
type: "Q8_1",
|
||||
type_id: 9,
|
||||
shape: ["320"],
|
||||
file_offset: fixture.dataOffset + 32,
|
||||
byte_length: null,
|
||||
byte_length_state: "unknown",
|
||||
storage_validation: "unknown",
|
||||
};
|
||||
expect(report.tensors[1]).toMatchObject(weight);
|
||||
expect(report.totals).toMatchObject({
|
||||
parameter_count: "324",
|
||||
tensor_bytes: null,
|
||||
by_type: [
|
||||
{ type: "F32", bytes: 16 },
|
||||
{ type: "Q8_1", bytes: null },
|
||||
],
|
||||
});
|
||||
const coverage = { storage_sizes: "partial", tensor_ranges: "partial" };
|
||||
expect(report.coverage).toMatchObject(coverage);
|
||||
const limitation = expect.stringContaining(
|
||||
"Q8_1 has released 40-byte and 36-byte layouts",
|
||||
);
|
||||
expect(report.limitations).toContainEqual(limitation);
|
||||
|
||||
const response = await call("inspect_model_artifact", {
|
||||
path,
|
||||
hash: "full",
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
const summary = modelArtifactSummaryViewResultSchema.parse(
|
||||
parseEvidence(response.structuredContent).normalized_result,
|
||||
);
|
||||
expect(summary.summary).toMatchObject({
|
||||
artifact: { path, sha256 },
|
||||
totals: { parameter_count: "324", tensor_bytes: null },
|
||||
coverage,
|
||||
});
|
||||
expect(summary.summary.limitations).toContainEqual(limitation);
|
||||
expect(await readFile(path)).toEqual(bytes);
|
||||
await unlink(path);
|
||||
|
||||
const source = {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: summary.parent_evidence_id,
|
||||
};
|
||||
const tensorResponse = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: {
|
||||
kind: "model-artifact",
|
||||
collection: "tensors",
|
||||
types: ["Q8_1"],
|
||||
storage_validation: ["unknown"],
|
||||
fields: Object.keys(weight).filter((field) => field !== "index"),
|
||||
},
|
||||
});
|
||||
expect(tensorResponse.isError, JSON.stringify(tensorResponse)).not.toBe(
|
||||
true,
|
||||
);
|
||||
const tensorView = analysisViewResultSchema.parse(
|
||||
parseEvidence(tensorResponse.structuredContent).normalized_result,
|
||||
);
|
||||
if (tensorView.kind !== "model-artifact")
|
||||
throw new Error("Incorrect tensor view");
|
||||
expect(tensorView.items).toEqual([weight]);
|
||||
expect(tensorView.source).toMatchObject({ path, sha256 });
|
||||
expect(tensorView.source_coverage).toMatchObject(coverage);
|
||||
expect(tensorView.limitations).toContainEqual(limitation);
|
||||
|
||||
const typeResponse = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: {
|
||||
kind: "model-artifact",
|
||||
collection: "types",
|
||||
types: ["Q8_1"],
|
||||
fields: ["type", "parameter_count", "bytes"],
|
||||
},
|
||||
});
|
||||
expect(typeResponse.isError, JSON.stringify(typeResponse)).not.toBe(true);
|
||||
const typeView = analysisViewResultSchema.parse(
|
||||
parseEvidence(typeResponse.structuredContent).normalized_result,
|
||||
);
|
||||
if (typeView.kind !== "model-artifact")
|
||||
throw new Error("Incorrect type view");
|
||||
expect(typeView.items).toEqual([
|
||||
{ index: 1, type: "Q8_1", parameter_count: "320", bytes: null },
|
||||
]);
|
||||
expect(typeView.source_coverage).toMatchObject(coverage);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const assertRetainedLayoutViews = async (
|
||||
call: Awaited<ReturnType<typeof connectLocalToolsMcp>>["call"],
|
||||
parentEvidenceId: string,
|
||||
report: ModelArtifact,
|
||||
sha256: string,
|
||||
): Promise<void> => {
|
||||
const { path } = report.artifact;
|
||||
const { header, tensor_ranges, storage_sizes } = report.coverage;
|
||||
const coverage = { header, tensor_ranges, storage_sizes };
|
||||
const source = {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parentEvidenceId,
|
||||
};
|
||||
const types = ["Q2_K", "AMBIGUOUS_16", "AMBIGUOUS_24", "AMBIGUOUS_25"];
|
||||
const tensorResponse = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: {
|
||||
kind: "model-artifact",
|
||||
collection: "tensors",
|
||||
types,
|
||||
storage_validation: ["unknown"],
|
||||
fields: [
|
||||
"type",
|
||||
"type_id",
|
||||
"shape",
|
||||
"file_offset",
|
||||
"byte_length",
|
||||
"storage_validation",
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(tensorResponse.isError, JSON.stringify(tensorResponse)).not.toBe(true);
|
||||
const tensorView = analysisViewResultSchema.parse(
|
||||
parseEvidence(tensorResponse.structuredContent).normalized_result,
|
||||
);
|
||||
if (tensorView.kind !== "model-artifact")
|
||||
throw new Error("Incorrect tensor view");
|
||||
expect(tensorView.items).toEqual(
|
||||
report.tensors
|
||||
.filter((tensor) => types.includes(tensor.type))
|
||||
.map(
|
||||
({
|
||||
index,
|
||||
type,
|
||||
type_id,
|
||||
shape,
|
||||
file_offset,
|
||||
byte_length,
|
||||
storage_validation,
|
||||
}) => ({
|
||||
index,
|
||||
type,
|
||||
type_id,
|
||||
shape,
|
||||
file_offset,
|
||||
byte_length,
|
||||
storage_validation,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(tensorView.source).toMatchObject({ path, sha256 });
|
||||
expect(tensorView.source_coverage).toMatchObject(coverage);
|
||||
const typeResponse = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: {
|
||||
kind: "model-artifact",
|
||||
collection: "types",
|
||||
types,
|
||||
fields: ["type", "bytes"],
|
||||
},
|
||||
});
|
||||
expect(typeResponse.isError, JSON.stringify(typeResponse)).not.toBe(true);
|
||||
const typeView = analysisViewResultSchema.parse(
|
||||
parseEvidence(typeResponse.structuredContent).normalized_result,
|
||||
);
|
||||
if (typeView.kind !== "model-artifact")
|
||||
throw new Error("Incorrect type view");
|
||||
expect(typeView.items).toEqual(
|
||||
report.totals.by_type.flatMap((row, index) =>
|
||||
types.includes(row.type) ? [{ index, type: row.type, bytes: null }] : [],
|
||||
),
|
||||
);
|
||||
expect(typeView.source_coverage).toMatchObject(coverage);
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"preserves released K and shifted integer/IQ uncertainty through CLI and retained MCP views",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-model-historical-layouts-");
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const layout of ["legacy", "modern"] as const) {
|
||||
const path = join(root, `${layout}.gguf`);
|
||||
const base = ggufFixture();
|
||||
// Authored bodies follow released source sizes; historical producers are
|
||||
// not executed. Both files carry the same version and global file type.
|
||||
const declarations = [
|
||||
{ name: "control", type: 0, shape: [4n], bytes: 16, label: "F32" },
|
||||
{
|
||||
name: "stable-integer",
|
||||
type: 26,
|
||||
shape: [3n],
|
||||
bytes: 12,
|
||||
label: "I32",
|
||||
},
|
||||
...ggufKLayouts.flatMap(({ id, name, bytes64, bytes256 }) =>
|
||||
(layout === "legacy" ? [64, 256] : [256]).map((elements) => ({
|
||||
name: `${name}-${String(elements)}`,
|
||||
type: id,
|
||||
shape: [BigInt(elements)],
|
||||
bytes: layout === "legacy" ? (elements / 64) * bytes64 : bytes256,
|
||||
label: name,
|
||||
})),
|
||||
),
|
||||
...ggufShiftedTypeLayouts.map(
|
||||
({ id, integerBytes, modernBytes256 }) => ({
|
||||
name: `shifted-${String(id)}`,
|
||||
type: id,
|
||||
shape: [256n],
|
||||
bytes: layout === "legacy" ? 256 * integerBytes : modernBytes256,
|
||||
label: `AMBIGUOUS_${String(id)}`,
|
||||
}),
|
||||
),
|
||||
];
|
||||
let nextOffset = 0n;
|
||||
const tensors = declarations.map((declaration) => {
|
||||
const tensor = { ...declaration, offset: nextOffset };
|
||||
nextOffset += BigInt(Math.ceil(declaration.bytes / 32) * 32);
|
||||
return tensor;
|
||||
});
|
||||
const fixture = ggufFixture({
|
||||
metadata: [
|
||||
{ key: "general.quantization_version", type: 4, value: base.u32(2) },
|
||||
{ key: "general.file_type", type: 4, value: base.u32(15) },
|
||||
],
|
||||
tensors,
|
||||
});
|
||||
await writeFile(path, fixture.bytes);
|
||||
const response = await cli.run({
|
||||
arguments: [
|
||||
"inspect-model-artifact",
|
||||
path,
|
||||
"--detail",
|
||||
"complete",
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
const report = modelArtifactSchema.parse(
|
||||
parseEvidence(response.json).normalized_result,
|
||||
);
|
||||
expect(report.tensors.slice(0, 2)).toMatchObject([
|
||||
{ type: "F32", byte_length: 16, storage_validation: "consistent" },
|
||||
{
|
||||
type: "I32",
|
||||
type_id: 26,
|
||||
byte_length: 12,
|
||||
storage_validation: "consistent",
|
||||
},
|
||||
]);
|
||||
const unresolved = tensors.slice(2).map((tensor, index) => ({
|
||||
index: index + 2,
|
||||
type: tensor.label,
|
||||
type_id: tensor.type,
|
||||
shape: tensor.shape.map(String),
|
||||
file_offset: fixture.dataOffset + Number(tensor.offset),
|
||||
byte_length: null,
|
||||
storage_validation: "unknown",
|
||||
}));
|
||||
expect(report.tensors.slice(2)).toMatchObject(unresolved);
|
||||
const coverage = {
|
||||
header: "complete",
|
||||
tensor_ranges: "partial",
|
||||
storage_sizes: "partial",
|
||||
};
|
||||
expect(report.coverage).toMatchObject(coverage);
|
||||
expect(report.totals.tensor_bytes).toBeNull();
|
||||
for (const { id, modernName } of ggufShiftedTypeLayouts) {
|
||||
expect(report.limitations).toContainEqual(
|
||||
expect.stringMatching(
|
||||
new RegExp(`type ID ${String(id)} has released .*${modernName}`),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const mcpResponse = await call("inspect_model_artifact", {
|
||||
path,
|
||||
hash: "full",
|
||||
});
|
||||
expect(mcpResponse.isError, JSON.stringify(mcpResponse)).not.toBe(true);
|
||||
const summary = modelArtifactSummaryViewResultSchema.parse(
|
||||
parseEvidence(mcpResponse.structuredContent).normalized_result,
|
||||
);
|
||||
const sha256 = createHash("sha256").update(fixture.bytes).digest("hex");
|
||||
expect(summary.summary).toMatchObject({
|
||||
artifact: { path, sha256 },
|
||||
totals: { tensor_bytes: null },
|
||||
coverage,
|
||||
});
|
||||
for (const limitation of report.limitations.filter(
|
||||
(reason) =>
|
||||
reason.includes("have released") || reason.includes("has released"),
|
||||
))
|
||||
expect(summary.summary.limitations).toContain(limitation);
|
||||
expect(await readFile(path)).toEqual(fixture.bytes);
|
||||
await unlink(path);
|
||||
await assertRetainedLayoutViews(
|
||||
call,
|
||||
summary.parent_evidence_id,
|
||||
report,
|
||||
sha256,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves exact large safetensors dimensions while bounding actual offsets",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -17,7 +17,12 @@ import { GgufHeader } from "../../../src/model/GgufHeader.js";
|
||||
import { createModelArtifactService } from "../../../src/composition/model.js";
|
||||
import { modelArtifactSchema } from "../../../src/domain/model/modelArtifact.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { ggufFixture, safetensorsFixture } from "../../fixtures/model/model.js";
|
||||
import {
|
||||
ggufFixture,
|
||||
ggufKLayouts,
|
||||
ggufShiftedTypeLayouts,
|
||||
safetensorsFixture,
|
||||
} from "../../fixtures/model/model.js";
|
||||
|
||||
const hash = (bytes: Buffer) =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
@@ -197,6 +202,7 @@ it.each([false, true])(
|
||||
it.each([
|
||||
{ name: "excess rank", type: 0, shape: [1n, 1n, 1n, 1n, 1n], bytes: 4 },
|
||||
{ name: "quantized scalar", type: 2, shape: [], bytes: 0 },
|
||||
{ name: "Q8_1 incomplete block", type: 9, shape: [31n], bytes: 40 },
|
||||
])(
|
||||
"rejects GGUF $name without relaxing representation constraints",
|
||||
async (tensor) => {
|
||||
@@ -443,6 +449,239 @@ it("returns unknown lengths for unknown GGUF types without inventing storage siz
|
||||
expect(result.value.totals.tensor_bytes).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ version: 2, bigEndian: false, blockBytes: 36 },
|
||||
{ version: 2, bigEndian: false, blockBytes: 40 },
|
||||
{ version: 3, bigEndian: false, blockBytes: 36 },
|
||||
{ version: 3, bigEndian: false, blockBytes: 40 },
|
||||
{ version: 3, bigEndian: true, blockBytes: 36 },
|
||||
{ version: 3, bigEndian: true, blockBytes: 40 },
|
||||
])(
|
||||
"preserves unresolved Q8_1 storage for GGUF v$version, endian=$bigEndian, block bytes=$blockBytes",
|
||||
async ({ version, bigEndian, blockBytes }) => {
|
||||
const base = ggufFixture({ bigEndian });
|
||||
const fixture = ggufFixture({
|
||||
version,
|
||||
bigEndian,
|
||||
metadata: [
|
||||
{ key: "general.quantization_version", type: 4, value: base.u32(2) },
|
||||
],
|
||||
tensors: [
|
||||
{ name: "control", type: 0, shape: [4n], offset: 0n, bytes: 16 },
|
||||
{
|
||||
name: "weight",
|
||||
type: 9,
|
||||
shape: [320n],
|
||||
offset: 32n,
|
||||
bytes: 10 * blockBytes,
|
||||
},
|
||||
],
|
||||
});
|
||||
const { provider, path } = await providerAt(
|
||||
Buffer.concat([fixture.bytes, Buffer.alloc(16)]),
|
||||
);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.tensors).toMatchObject([
|
||||
{ name: "control", byte_length: 16, storage_validation: "consistent" },
|
||||
{
|
||||
name: "weight",
|
||||
type: "Q8_1",
|
||||
type_id: 9,
|
||||
shape: ["320"],
|
||||
data_offset: 32,
|
||||
file_offset: fixture.dataOffset + 32,
|
||||
parameter_count: "320",
|
||||
byte_length: null,
|
||||
byte_length_state: "unknown",
|
||||
storage_validation: "unknown",
|
||||
},
|
||||
]);
|
||||
expect(result.value.totals).toMatchObject({
|
||||
parameter_count: "324",
|
||||
tensor_bytes: null,
|
||||
by_type: [
|
||||
{ type: "F32", bytes: 16, parameter_count: "4" },
|
||||
{ type: "Q8_1", bytes: null, parameter_count: "320" },
|
||||
],
|
||||
});
|
||||
expect(result.value.coverage).toMatchObject({
|
||||
header: "complete",
|
||||
tensor_ranges: "partial",
|
||||
storage_sizes: "partial",
|
||||
});
|
||||
expect(result.value.limitations).toContainEqual(
|
||||
expect.stringContaining("Q8_1 has released 40-byte and 36-byte layouts"),
|
||||
);
|
||||
expect(result.value.limitations.join(" ")).not.toContain(
|
||||
"Unknown tensor type Q8_1",
|
||||
);
|
||||
expect(result.value.artifact.bytes_read).toBe(fixture.header.length);
|
||||
},
|
||||
);
|
||||
|
||||
it("derives empty Q8_1 storage without choosing a historical block layout", async () => {
|
||||
const fixture = ggufFixture({
|
||||
metadata: [],
|
||||
tensors: [
|
||||
{ name: "empty", type: 9, shape: [32n, 0n], offset: 0n, bytes: 0 },
|
||||
],
|
||||
});
|
||||
const { provider, path } = await providerAt(fixture.bytes);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.tensors[0]).toMatchObject({
|
||||
type: "Q8_1",
|
||||
parameter_count: "0",
|
||||
byte_length: 0,
|
||||
byte_length_state: "derived",
|
||||
storage_validation: "consistent",
|
||||
});
|
||||
expect(result.value.totals.tensor_bytes).toBe(0);
|
||||
expect(result.value.coverage).toMatchObject({
|
||||
tensor_ranges: "complete",
|
||||
storage_sizes: "complete",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(ggufKLayouts)(
|
||||
"preserves $name identity while leaving released K64/K256 storage unresolved",
|
||||
async ({ id, name, bytes64, bytes256 }) => {
|
||||
// The released GGUF writer pads each tensor to 32 bytes. For 256 elements,
|
||||
// both layouts have the same padded extent, despite different byte lengths.
|
||||
expect(Math.ceil((4 * bytes64) / 32)).toBe(Math.ceil(bytes256 / 32));
|
||||
for (const [elements, bytes] of [
|
||||
[64, bytes64],
|
||||
[256, 4 * bytes64],
|
||||
[256, bytes256],
|
||||
] as const) {
|
||||
const base = ggufFixture();
|
||||
const fixture = ggufFixture({
|
||||
metadata: [
|
||||
{ key: "general.quantization_version", type: 4, value: base.u32(2) },
|
||||
{ key: "general.file_type", type: 4, value: base.u32(15) },
|
||||
],
|
||||
tensors: [
|
||||
{
|
||||
name: "weight",
|
||||
type: id,
|
||||
shape: [BigInt(elements)],
|
||||
offset: 0n,
|
||||
bytes,
|
||||
},
|
||||
],
|
||||
});
|
||||
const { provider, path } = await providerAt(
|
||||
Buffer.concat([
|
||||
fixture.bytes,
|
||||
Buffer.alloc(Math.ceil(bytes / 32) * 32 - bytes),
|
||||
]),
|
||||
);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.tensors[0]).toMatchObject({
|
||||
type: name,
|
||||
type_id: id,
|
||||
shape: [String(elements)],
|
||||
parameter_count: String(elements),
|
||||
byte_length: null,
|
||||
storage_validation: "unknown",
|
||||
});
|
||||
expect(result.value.coverage).toMatchObject({
|
||||
tensor_ranges: "partial",
|
||||
storage_sizes: "partial",
|
||||
});
|
||||
expect(result.value.totals.tensor_bytes).toBeNull();
|
||||
expect(result.value.limitations).toContainEqual(
|
||||
expect.stringContaining("common 64-element row constraint"),
|
||||
);
|
||||
}
|
||||
const { provider, path } = await providerAt(
|
||||
ggufFixture({
|
||||
tensors: [
|
||||
{
|
||||
name: "invalid",
|
||||
type: id,
|
||||
shape: [32n],
|
||||
offset: 0n,
|
||||
bytes: bytes64,
|
||||
},
|
||||
],
|
||||
}).bytes,
|
||||
);
|
||||
const rejected = await provider.inspect({ path, hash: "header" });
|
||||
expect(rejected.ok).toBe(false);
|
||||
if (rejected.ok) throw new Error("Incomplete common K row admitted");
|
||||
expect(rejected.error).toMatchObject({
|
||||
constraint: "file_format_rejected",
|
||||
reason: expect.stringContaining("multiple of 64 elements"),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(ggufShiftedTypeLayouts)(
|
||||
"retains ambiguous numeric type $id for released integer and $modernName bodies",
|
||||
async ({ id, integerBytes, modernName, modernBytes256 }) => {
|
||||
for (const [elements, bytes] of [
|
||||
[3, 3 * integerBytes],
|
||||
[256, 256 * integerBytes],
|
||||
[256, modernBytes256],
|
||||
[0, 0],
|
||||
] as const) {
|
||||
const fixture = ggufFixture({
|
||||
tensors: [
|
||||
{
|
||||
name: "weight",
|
||||
type: id,
|
||||
shape: [BigInt(elements)],
|
||||
offset: 0n,
|
||||
bytes,
|
||||
},
|
||||
],
|
||||
});
|
||||
const { provider, path } = await providerAt(fixture.bytes);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.tensors[0]).toMatchObject({
|
||||
type: `AMBIGUOUS_${String(id)}`,
|
||||
type_id: id,
|
||||
shape: [String(elements)],
|
||||
parameter_count: String(elements),
|
||||
byte_length: elements === 0 ? 0 : null,
|
||||
storage_validation: elements === 0 ? "consistent" : "unknown",
|
||||
});
|
||||
expect(result.value.limitations).toContainEqual(
|
||||
expect.stringContaining(`type ID ${String(id)} has released`),
|
||||
);
|
||||
expect(result.value.limitations.join(" ")).toContain(modernName);
|
||||
expect(result.value.totals.tensor_bytes).toBe(elements === 0 ? 0 : null);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("retains established storage for stable GGUF types and zero-byte K declarations", async () => {
|
||||
const fixture = ggufFixture({
|
||||
tensors: [
|
||||
{ name: "integer", type: 26, shape: [3n], offset: 0n, bytes: 12 },
|
||||
{ name: "empty-k", type: 10, shape: [64n, 0n], offset: 32n, bytes: 0 },
|
||||
{ name: "quant", type: 2, shape: [32n], offset: 32n, bytes: 18 },
|
||||
],
|
||||
});
|
||||
const { provider, path } = await providerAt(fixture.bytes);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.tensors).toMatchObject([
|
||||
{ type: "I32", byte_length: 12, storage_validation: "consistent" },
|
||||
{ type: "Q2_K", byte_length: 0, storage_validation: "consistent" },
|
||||
{ type: "Q4_0", byte_length: 18, storage_validation: "consistent" },
|
||||
]);
|
||||
expect(result.value.totals.tensor_bytes).toBe(30);
|
||||
expect(result.value.coverage).toMatchObject({
|
||||
tensor_ranges: "complete",
|
||||
storage_sizes: "complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps unknown safetensors storage validation separate from observed valid byte ranges", async () => {
|
||||
const fixture = safetensorsFixture(
|
||||
'{"future":{"dtype":"FUTURE4","shape":[2],"data_offsets":[0,4]}}',
|
||||
|
||||
@@ -131,7 +131,7 @@ it.each([undefined, 64])(
|
||||
},
|
||||
);
|
||||
|
||||
it("retains completed GGUF tensor declarations before later tensor bytes cancel", async () => {
|
||||
it("retains GGUF tensor declarations and layout limitations before later tensor bytes cancel", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-model-tensor-partial-"));
|
||||
const path = join(root, "model.gguf");
|
||||
const tensorCount = 4000;
|
||||
@@ -139,10 +139,10 @@ it("retains completed GGUF tensor declarations before later tensor bytes cancel"
|
||||
metadata: [],
|
||||
tensors: Array.from({ length: tensorCount }, (_, index) => ({
|
||||
name: `tensor-${String(index).padStart(4, "0")}`,
|
||||
type: 0,
|
||||
shape: [1n],
|
||||
type: index === 0 ? 9 : 0,
|
||||
shape: index === 0 ? [320n] : [1n],
|
||||
offset: 0n,
|
||||
bytes: 4,
|
||||
bytes: index === 0 ? 400 : 4,
|
||||
})),
|
||||
});
|
||||
await writeFile(path, fixture.bytes);
|
||||
@@ -210,12 +210,18 @@ it("retains completed GGUF tensor declarations before later tensor bytes cancel"
|
||||
expect(partial.tensor_declarations.length).toBeGreaterThan(0);
|
||||
expect(partial.tensor_declarations[0]).toMatchObject({
|
||||
index: 0,
|
||||
type: "Q8_1",
|
||||
byte_length: null,
|
||||
byte_length_state: "unknown",
|
||||
file_offset: null,
|
||||
storage_validation: "not-established",
|
||||
});
|
||||
expect(partial.tensors).toEqual([]);
|
||||
expect(partial.coverage.tensors).toBe("partial");
|
||||
expect(evidence.normalized_result).toMatchObject({ alignment: 32 });
|
||||
expect(evidence.limitations).toContainEqual(
|
||||
expect.stringContaining("Q8_1 has released 40-byte and 36-byte layouts"),
|
||||
);
|
||||
expect(partial.artifact).toMatchObject({
|
||||
path,
|
||||
header_sha256: null,
|
||||
@@ -244,7 +250,7 @@ it.each([
|
||||
JSON.stringify({
|
||||
__metadata__: { author: "test" },
|
||||
weight: {
|
||||
dtype: "F32",
|
||||
dtype: "FUTURE_F32",
|
||||
shape: [524288],
|
||||
data_offsets: [0, 2 * 1024 * 1024],
|
||||
},
|
||||
@@ -330,6 +336,14 @@ it.each([
|
||||
file_identity: "unverified",
|
||||
},
|
||||
});
|
||||
if (format === "safetensors")
|
||||
expect(
|
||||
parseEvidence(result.error.partialObservation).limitations,
|
||||
).toContainEqual(
|
||||
expect.stringContaining(
|
||||
"Unknown tensor types prevent storage-size validation: FUTURE_F32",
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
readSpy.mockRestore();
|
||||
await provider.close();
|
||||
|
||||
Vendored
+28
@@ -1,3 +1,31 @@
|
||||
// Released llama.cpp b1500 GGML_QKK_64 blocks and default/b3001 blocks.
|
||||
// These are source-derived payload sizes, not output from historical binaries.
|
||||
export const ggufKLayouts = [
|
||||
{ id: 10, name: "Q2_K", bytes64: 24, bytes256: 84 },
|
||||
{ id: 11, name: "Q3_K", bytes64: 28, bytes256: 110 },
|
||||
{ id: 12, name: "Q4_K", bytes64: 38, bytes256: 144 },
|
||||
{ id: 13, name: "Q5_K", bytes64: 46, bytes256: 176 },
|
||||
{ id: 14, name: "Q6_K", bytes64: 54, bytes256: 210 },
|
||||
{ id: 15, name: "Q8_K", bytes64: 76, bytes256: 292 },
|
||||
] as const;
|
||||
|
||||
// Integer IDs shifted as IQ types were added, before upstream PR 6050 pinned
|
||||
// integers at 24–26. Oldest colliding integer width and modern bytes/256 values
|
||||
// are independent goldens from b1500/b1803/b1871/b2100/b2172/b2221/b2253/b2275
|
||||
// and b3001. An authored body does not establish producer provenance.
|
||||
export const ggufShiftedTypeLayouts = [
|
||||
{ id: 16, integerBytes: 1, modernName: "IQ2_XXS", modernBytes256: 66 },
|
||||
{ id: 17, integerBytes: 2, modernName: "IQ2_XS", modernBytes256: 74 },
|
||||
{ id: 18, integerBytes: 4, modernName: "IQ3_XXS", modernBytes256: 98 },
|
||||
{ id: 19, integerBytes: 4, modernName: "IQ1_S", modernBytes256: 50 },
|
||||
{ id: 20, integerBytes: 4, modernName: "IQ4_NL", modernBytes256: 144 },
|
||||
{ id: 21, integerBytes: 4, modernName: "IQ3_S", modernBytes256: 110 },
|
||||
{ id: 22, integerBytes: 4, modernName: "IQ2_S", modernBytes256: 82 },
|
||||
{ id: 23, integerBytes: 4, modernName: "IQ4_XS", modernBytes256: 136 },
|
||||
{ id: 24, integerBytes: 4, modernName: "I8", modernBytes256: 256 },
|
||||
{ id: 25, integerBytes: 4, modernName: "I16", modernBytes256: 512 },
|
||||
] as const;
|
||||
|
||||
/** Source-built declarations with independent expected byte layouts; no model loader. */
|
||||
export const ggufFixture = (
|
||||
options: {
|
||||
|
||||
Reference in New Issue
Block a user