Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ All notable changes to this project are documented in this file.
crates: `[dependencies]` stays empty for this feature; the upstream
`safetensors` crate, `serde_json`, and `corinth-canal` are not used. Does
not include payload mmap or Hugging Face `config.json` policy.
- **GGUF `ParseLimits` (RM-1358):** documented resource budgets for untrusted
GGUF headers — KV count, tensor count, string bytes, array work items,
tensor rank, and metadata bytes. File-declared `u64` sizes convert with
typed [`HostSizeField`] errors; exhausted budgets return
[`ParserError::LimitExceeded`] naming the limit. Alignment, tensor offsets,
element counts, and packed byte sizes use checked arithmetic. Default and
`mmap` readers share the same policy. Trusted callers override via
`load_gguf_with_limits` / `parse_bytes_with_limits` /
`load_gguf_mmap_with_limits` without changing default safety.
- **Safetensors shard-index invariants ([RM-1360](https://linear.app/rpd-34/issue/RM-1360)):**
index shard paths resolve relative to the checkpoint root; absolute paths
and `..` traversal that would leave the root are rejected; existing paths
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Today, `engram-parser` ships GGUF v3 deserialization, per-expert raw-weight extr
### Shipped now — GGUF

- Parses GGUF v3 magic, header, KV metadata, and tensor directory into an in-memory [`GgufLayout`].
- Applies a documented [`ParseLimits`] budget (KV/tensor counts, string sizes, array work, tensor rank, metadata bytes) before allocation or loops proportional to file-declared values. Defaults are generous; trusted callers can override via `load_gguf_with_limits` / `parse_bytes_with_limits` without weakening the default path.
- Enumerates MoE experts discovered in a checkpoint.
- Extracts the raw byte buffers for one expert's `gate`, `up`, and `down` projections.
- Supports stacked (`blk.{B}.ffn_{role}_exps.weight`) and per-expert (`blk.{B}.ffn_{role}.{E}.weight`) conventions.
Expand Down Expand Up @@ -220,13 +221,13 @@ Numeric helpers: `dequantize_f16`, `dequantize_q8_0`, `dequantize_q5_k`, `dequan

Current GGUF surface includes:

- `load_gguf`, `parse_bytes`;
- `#[cfg(feature = "mmap")] load_gguf_mmap` → `GgufLayoutMmap` (page-aligned tensor slices via `tensor_page_aligned_bytes`);
- `GgufLayout`, `GgufMetadata`, `Tensor`, `DType`;
- `load_gguf`, `parse_bytes` (and `*_with_limits` for an explicit [`ParseLimits`] policy);
- `#[cfg(feature = "mmap")] load_gguf_mmap` / `load_gguf_mmap_with_limits` → `GgufLayoutMmap` (page-aligned tensor slices via `tensor_page_aligned_bytes`);
- `GgufLayout`, `GgufMetadata`, `Tensor`, `DType`, `ParseLimits`;
- `dequantize_f16` (on `Tensor`), `dequantize_q8_0`, `dequantize_q5_k`, `dequantize_q6_k`, `dequantize_iq3_m`;
- `extract_expert`, `list_experts`;
- `MoeExpertWeights`, `RawTensor`;
- `ParserError`, `Result`;
- `ParserError`, `ParseLimitKind`, `HostSizeField`, `Result`;
- public `GGML_TYPE_*` / `GGUF_VALUE_TYPE_*` constants and the `ggml_type_label` label function.

Safetensors surface (`--features safetensors`) includes:
Expand Down
204 changes: 204 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,86 @@
use std::fmt;
use std::io;

/// Which [`crate::ParseLimits`] field was exhausted.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ParseLimitKind {
/// [`crate::ParseLimits::max_kv_count`].
KvCount,
/// [`crate::ParseLimits::max_tensor_count`].
TensorCount,
/// [`crate::ParseLimits::max_string_bytes`].
StringBytes,
/// [`crate::ParseLimits::max_array_work_items`].
ArrayWorkItems,
/// [`crate::ParseLimits::max_tensor_rank`].
TensorRank,
/// [`crate::ParseLimits::max_metadata_bytes`].
MetadataBytes,
}

impl ParseLimitKind {
/// Stable name used in parser errors and tests.
pub const fn as_str(self) -> &'static str {
match self {
Self::KvCount => "max_kv_count",
Self::TensorCount => "max_tensor_count",
Self::StringBytes => "max_string_bytes",
Self::ArrayWorkItems => "max_array_work_items",
Self::TensorRank => "max_tensor_rank",
Self::MetadataBytes => "max_metadata_bytes",
}
}
}

impl fmt::Display for ParseLimitKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

/// File-declared field that failed `u64 -> usize` conversion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HostSizeField {
/// Length prefix of a GGUF string (KV key, string value, or tensor name).
StringLen,
/// Header `kv_count`.
KvCount,
/// Header `tensor_count`.
TensorCount,
/// Tensor directory `n_dims`.
TensorRank,
/// One tensor dimension.
TensorDim,
/// Tensor `relative_offset` in the data region.
RelativeOffset,
/// `general.alignment` layout field.
Alignment,
/// GGUF array element count.
ArrayLen,
}

impl HostSizeField {
/// Stable name used in parser errors and tests.
pub const fn as_str(self) -> &'static str {
match self {
Self::StringLen => "string_len",
Self::KvCount => "kv_count",
Self::TensorCount => "tensor_count",
Self::TensorRank => "tensor_rank",
Self::TensorDim => "tensor_dim",
Self::RelativeOffset => "relative_offset",
Self::Alignment => "general.alignment",
Self::ArrayLen => "array_len",
}
}
}

impl fmt::Display for HostSizeField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

/// Unified error type for checkpoint parsing and MoE weight extraction.
#[derive(Debug)]
pub enum ParserError {
Expand Down Expand Up @@ -52,6 +132,26 @@ pub enum ParserError {
/// Number of experts actually available.
available: usize,
},
/// A [`crate::ParseLimits`] budget was exhausted by a file-declared value.
LimitExceeded {
/// Path of the checkpoint.
path: String,
/// Budget that was exhausted.
limit: ParseLimitKind,
/// Declared or accumulated value that exceeded the budget.
declared: u64,
/// Configured budget for `limit`.
budget: u64,
},
/// A file-declared `u64` could not be represented as a host `usize`.
HostSizeOverflow {
/// Path of the checkpoint.
path: String,
/// On-wire field that overflowed.
field: HostSizeField,
/// Declared value that does not fit in `usize`.
value: u64,
},
/// A tensor name is claimed by more than one Safetensors shard.
DuplicateTensorOwnership {
/// Tensor name with conflicting owners.
Expand All @@ -70,6 +170,30 @@ pub enum ParserError {
},
}

impl ParserError {
pub(crate) fn limit_exceeded(
path: impl Into<String>,
limit: ParseLimitKind,
declared: u64,
budget: u64,
) -> Self {
Self::LimitExceeded {
path: path.into(),
limit,
declared,
budget,
}
}

pub(crate) fn host_size(path: impl Into<String>, field: HostSizeField, value: u64) -> Self {
Self::HostSizeOverflow {
path: path.into(),
field,
value,
}
}
}

impl fmt::Display for ParserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Expand All @@ -91,6 +215,19 @@ impl fmt::Display for ParserError {
f,
"expert index out of range: block={block}, expert={expert}, available={available}"
),
Self::LimitExceeded {
path,
limit,
declared,
budget,
} => write!(
f,
"parse limit {limit} exceeded in '{path}': declared {declared}, budget {budget}"
),
Self::HostSizeOverflow { path, field, value } => write!(
f,
"host-size conversion failed in '{path}': field {field} value {value} does not fit usize"
),
Self::DuplicateTensorOwnership { name, shards, path } => {
write!(
f,
Expand All @@ -113,6 +250,8 @@ impl std::error::Error for ParserError {
| Self::MissingTensor { .. }
| Self::InvalidLayout { .. }
| Self::ExpertOutOfRange { .. }
| Self::LimitExceeded { .. }
| Self::HostSizeOverflow { .. }
| Self::DuplicateTensorOwnership { .. }
| Self::MissingShard { .. } => None,
}
Expand All @@ -121,3 +260,68 @@ impl std::error::Error for ParserError {

/// Convenience alias used throughout the crate.
pub type Result<T> = std::result::Result<T, ParserError>;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn limit_and_host_errors_name_the_field() {
let limit = ParserError::limit_exceeded("mem://e", ParseLimitKind::StringBytes, 17, 16);
let host = ParserError::host_size("mem://e", HostSizeField::RelativeOffset, u64::MAX);
assert!(limit.to_string().contains("max_string_bytes"));
assert!(host.to_string().contains("relative_offset"));
assert!(std::error::Error::source(&limit).is_none());
assert!(std::error::Error::source(&host).is_none());
}

#[test]
fn limit_kind_match_is_exhaustive() {
let kinds = [
ParseLimitKind::KvCount,
ParseLimitKind::TensorCount,
ParseLimitKind::StringBytes,
ParseLimitKind::ArrayWorkItems,
ParseLimitKind::TensorRank,
ParseLimitKind::MetadataBytes,
];
for kind in kinds {
let name = match kind {
ParseLimitKind::KvCount => "max_kv_count",
ParseLimitKind::TensorCount => "max_tensor_count",
ParseLimitKind::StringBytes => "max_string_bytes",
ParseLimitKind::ArrayWorkItems => "max_array_work_items",
ParseLimitKind::TensorRank => "max_tensor_rank",
ParseLimitKind::MetadataBytes => "max_metadata_bytes",
};
assert_eq!(kind.as_str(), name);
}
}

#[test]
fn host_field_match_is_exhaustive() {
let fields = [
HostSizeField::StringLen,
HostSizeField::KvCount,
HostSizeField::TensorCount,
HostSizeField::TensorRank,
HostSizeField::TensorDim,
HostSizeField::RelativeOffset,
HostSizeField::Alignment,
HostSizeField::ArrayLen,
];
for field in fields {
let name = match field {
HostSizeField::StringLen => "string_len",
HostSizeField::KvCount => "kv_count",
HostSizeField::TensorCount => "tensor_count",
HostSizeField::TensorRank => "tensor_rank",
HostSizeField::TensorDim => "tensor_dim",
HostSizeField::RelativeOffset => "relative_offset",
HostSizeField::Alignment => "general.alignment",
HostSizeField::ArrayLen => "array_len",
};
assert_eq!(field.as_str(), name);
}
}
}
Loading
Loading