ferrum_types/
numerical.rs1use std::{fmt, str::FromStr};
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum KvStorageFormat {
15 #[default]
16 F16,
17 Int8PerTokenHeadF32ScaleV1,
18}
19
20impl KvStorageFormat {
21 pub const fn as_str(self) -> &'static str {
22 match self {
23 Self::F16 => "f16",
24 Self::Int8PerTokenHeadF32ScaleV1 => "int8-per-token-head-f32-scale-v1",
25 }
26 }
27}
28
29impl std::fmt::Display for KvStorageFormat {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.write_str(self.as_str())
32 }
33}
34
35impl TryFrom<crate::KvCacheDtype> for KvStorageFormat {
36 type Error = String;
37
38 fn try_from(dtype: crate::KvCacheDtype) -> Result<Self, Self::Error> {
39 match dtype {
40 crate::KvCacheDtype::Fp16 => Ok(Self::F16),
41 crate::KvCacheDtype::Int8 => Ok(Self::Int8PerTokenHeadF32ScaleV1),
42 crate::KvCacheDtype::Bf16 => Err(
43 "vNext BF16 KV storage is unsupported; use fp16 or a supported int8 plan".into(),
44 ),
45 crate::KvCacheDtype::Fp8 => {
46 Err("vNext FP8 KV storage is unsupported; use fp16 or a supported int8 plan".into())
47 }
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
55#[serde(try_from = "String", into = "String")]
56pub struct NumericalProfileId(String);
57
58impl NumericalProfileId {
59 pub fn new(value: impl Into<String>) -> Result<Self, String> {
60 let value = value.into();
61 if value.is_empty()
62 || value.len() > 160
63 || !value.bytes().all(|byte| {
64 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
65 })
66 {
67 return Err("numerical profile identity needs 1..=160 portable ASCII bytes".to_owned());
68 }
69 Ok(Self(value))
70 }
71
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl fmt::Display for NumericalProfileId {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 formatter.write_str(&self.0)
80 }
81}
82
83impl TryFrom<String> for NumericalProfileId {
84 type Error = String;
85
86 fn try_from(value: String) -> Result<Self, Self::Error> {
87 Self::new(value)
88 }
89}
90
91impl FromStr for NumericalProfileId {
92 type Err = String;
93
94 fn from_str(value: &str) -> Result<Self, Self::Err> {
95 Self::new(value)
96 }
97}
98
99impl From<NumericalProfileId> for String {
100 fn from(value: NumericalProfileId) -> Self {
101 value.0
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum NumericalExecutionPolicy {
110 #[default]
111 Auto,
112 Require(NumericalProfileId),
113}
114
115impl FromStr for NumericalExecutionPolicy {
116 type Err = String;
117
118 fn from_str(value: &str) -> Result<Self, Self::Err> {
119 match value {
120 "auto" => Ok(Self::Auto),
121 _ => Ok(Self::Require(value.parse()?)),
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn kv_storage_configuration_preserves_default_and_rejects_unimplemented_formats() {
132 assert_eq!(KvStorageFormat::default(), KvStorageFormat::F16);
133 assert_eq!(
134 KvStorageFormat::try_from(crate::KvCacheDtype::Int8).unwrap(),
135 KvStorageFormat::Int8PerTokenHeadF32ScaleV1
136 );
137 for dtype in [crate::KvCacheDtype::Bf16, crate::KvCacheDtype::Fp8] {
138 assert!(KvStorageFormat::try_from(dtype).is_err());
139 }
140 for storage in [
141 KvStorageFormat::F16,
142 KvStorageFormat::Int8PerTokenHeadF32ScaleV1,
143 ] {
144 assert_eq!(
145 serde_json::from_value::<KvStorageFormat>(serde_json::to_value(storage).unwrap())
146 .unwrap(),
147 storage
148 );
149 }
150 }
151
152 #[test]
153 fn explicit_policy_preserves_identity_through_config_and_cli_parsing() {
154 let explicit: NumericalExecutionPolicy = "fixture.f32-master".parse().unwrap();
155 assert_eq!(
156 explicit,
157 NumericalExecutionPolicy::Require(
158 NumericalProfileId::new("fixture.f32-master").unwrap()
159 )
160 );
161 let json = serde_json::to_vec(&explicit).unwrap();
162 assert_eq!(
163 serde_json::from_slice::<NumericalExecutionPolicy>(&json).unwrap(),
164 explicit
165 );
166 assert_eq!(
167 "auto".parse::<NumericalExecutionPolicy>().unwrap(),
168 NumericalExecutionPolicy::Auto
169 );
170 }
171
172 #[test]
173 fn invalid_profile_ids_are_rejected_at_the_deserialization_boundary() {
174 for value in ["", "fixture f16", "fixture\nf16", "配置.f16"] {
175 assert!(value.parse::<NumericalExecutionPolicy>().is_err());
176 let json = serde_json::json!({ "require": value });
177 assert!(serde_json::from_value::<NumericalExecutionPolicy>(json).is_err());
178 }
179 }
180}