Text Generation
Safetensors
English
fleck
causal-language-model
custom-architecture
conversational
research
Fleck-S-100K / inference.py
brulee-1's picture
fix: publish HF model name and links
03f87b0 verified
Raw
History Blame Contribute Delete
16.6 kB
#!/usr/bin/env python3
"""Self-contained CPU/MPS greedy inference for the Fleck-S-100K Instruct export.
This file intentionally has no import from the Fleck-LM source tree. The model
architecture, chat template, and SafeTensors contract are reproduced here so
this directory can be copied from Hugging Face and used on its own.
"""
from __future__ import annotations
import argparse
import math
from collections.abc import Iterable
from pathlib import Path
from typing import cast
import torch
from safetensors import safe_open
from safetensors.torch import load_file
from torch import Tensor, nn
from tokenizers import Tokenizer
VOCAB_SIZE = 1024
CONTEXT_LENGTH = 2048
HIDDEN_SIZE = 64
INTERMEDIATE_SIZE = 128
PHYSICAL_BLOCKS = 2
EFFECTIVE_DEPTH = 4
QUERY_HEADS = 4
KV_HEADS = 2
HEAD_DIM = 16
EMBEDDING_RANK = 32
ROPE_THETA = 10000.0
BOS_ID, EOS_ID, PAD_ID, UNK_ID = 0, 1, 2, 3
SYSTEM_ID, USER_ID, ASSISTANT_ID, EOT_ID = 4, 5, 6, 7
EXECUTION_ORDER = (0, 1, 0, 1)
TOKENIZER_NAME = "Fleck-Tokenizer-1024"
MODEL_METADATA = {
"candidate_id": "Fleck-S-100K",
"canonical_dtype": "bfloat16",
"context_length": "2048",
"effective_depth": "4",
"embedding_rank": "32",
"head_dim": "16",
"kv_heads": "2",
"logical_execution_order": '["A", "B", "A", "B"]',
"parameter_count": "109384",
"physical_blocks": "2",
"query_heads": "4",
"tokenizer_name": TOKENIZER_NAME,
}
class RMSNorm(nn.Module):
def __init__(self, size: int, eps: float = 1e-5) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(size))
self.eps = eps
def forward(self, hidden: Tensor) -> Tensor:
variance = hidden.float().pow(2).mean(dim=-1, keepdim=True)
scale = torch.rsqrt(variance + self.eps).to(dtype=hidden.dtype)
return hidden * scale * self.weight
class Attention(nn.Module):
def __init__(self) -> None:
super().__init__()
self.q_proj = nn.Linear(HIDDEN_SIZE, QUERY_HEADS * HEAD_DIM, bias=False)
self.k_proj = nn.Linear(HIDDEN_SIZE, KV_HEADS * HEAD_DIM, bias=False)
self.v_proj = nn.Linear(HIDDEN_SIZE, KV_HEADS * HEAD_DIM, bias=False)
self.o_proj = nn.Linear(HIDDEN_SIZE, HIDDEN_SIZE, bias=False)
@staticmethod
def _rope(value: Tensor, positions: Tensor) -> Tensor:
half = HEAD_DIM // 2
frequencies = torch.arange(half, device=value.device, dtype=torch.float32)
frequencies = ROPE_THETA ** (-2 * frequencies / HEAD_DIM)
angles = positions.float().unsqueeze(-1) * frequencies
cos = angles.cos().to(dtype=value.dtype)[None, None, :, :]
sin = angles.sin().to(dtype=value.dtype)[None, None, :, :]
first, second = value[..., :half], value[..., half:]
return torch.cat((first * cos - second * sin, first * sin + second * cos), dim=-1)
def forward(
self,
hidden: Tensor,
positions: Tensor,
past: tuple[Tensor, Tensor] | None = None,
attention_mask: Tensor | None = None,
) -> tuple[Tensor, tuple[Tensor, Tensor]]:
batch, length, _ = hidden.shape
query = self.q_proj(hidden).view(batch, length, QUERY_HEADS, HEAD_DIM).transpose(1, 2)
key = self.k_proj(hidden).view(batch, length, KV_HEADS, HEAD_DIM).transpose(1, 2)
value = self.v_proj(hidden).view(batch, length, KV_HEADS, HEAD_DIM).transpose(1, 2)
query = self._rope(query, positions)
key = self._rope(key, positions)
past_length = 0 if past is None else past[0].shape[2]
if past is not None:
key = torch.cat((past[0], key), dim=2)
value = torch.cat((past[1], value), dim=2)
total_length = key.shape[2]
repeats = QUERY_HEADS // KV_HEADS
expanded_key = key.repeat_interleave(repeats, dim=1)
expanded_value = value.repeat_interleave(repeats, dim=1)
scores = torch.matmul(query.float(), expanded_key.float().transpose(-1, -2))
scores = scores / math.sqrt(HEAD_DIM)
query_positions = torch.arange(
past_length, past_length + length, device=hidden.device
)[:, None]
key_positions = torch.arange(total_length, device=hidden.device)[None, :]
causal = key_positions <= query_positions
scores = scores.masked_fill(
~causal[None, None, :, :], torch.finfo(torch.float32).min
)
if attention_mask is not None:
if attention_mask.shape != (batch, total_length):
raise ValueError("attention_mask must have shape (batch, complete_kv_length)")
scores = scores.masked_fill(
~attention_mask.to(torch.bool)[:, None, None, :], torch.finfo(torch.float32).min
)
probabilities = torch.softmax(scores, dim=-1, dtype=torch.float32)
attended = torch.matmul(probabilities, expanded_value.float()).to(hidden.dtype)
attended = attended.transpose(1, 2).contiguous().view(batch, length, HIDDEN_SIZE)
return self.o_proj(attended), (key, value)
class PhysicalBlock(nn.Module):
def __init__(self) -> None:
super().__init__()
self.attention = Attention()
self.mlp_gate = nn.Linear(HIDDEN_SIZE, INTERMEDIATE_SIZE, bias=False)
self.mlp_up = nn.Linear(HIDDEN_SIZE, INTERMEDIATE_SIZE, bias=False)
self.mlp_down = nn.Linear(INTERMEDIATE_SIZE, HIDDEN_SIZE, bias=False)
class FleckModel(nn.Module):
def __init__(self) -> None:
super().__init__()
self.token_embedding = nn.Parameter(torch.empty(VOCAB_SIZE, EMBEDDING_RANK))
self.embedding_projection = nn.Parameter(torch.empty(EMBEDDING_RANK, HIDDEN_SIZE))
self.blocks = nn.ModuleList(PhysicalBlock() for _ in range(PHYSICAL_BLOCKS))
self.attention_norms = nn.ModuleList(
RMSNorm(HIDDEN_SIZE) for _ in range(EFFECTIVE_DEPTH)
)
self.mlp_norms = nn.ModuleList(RMSNorm(HIDDEN_SIZE) for _ in range(EFFECTIVE_DEPTH))
self.depth_embeddings = nn.Parameter(torch.empty(EFFECTIVE_DEPTH, HIDDEN_SIZE))
self.attention_residual_scales = nn.Parameter(torch.empty(EFFECTIVE_DEPTH))
self.mlp_residual_scales = nn.Parameter(torch.empty(EFFECTIVE_DEPTH))
self.final_norm = RMSNorm(HIDDEN_SIZE)
def _embed(self, input_ids: Tensor) -> Tensor:
return torch.nn.functional.embedding(input_ids, self.token_embedding) @ self.embedding_projection
def _logits(self, hidden: Tensor) -> Tensor:
rank_hidden = hidden.float() @ self.embedding_projection.float().t()
return rank_hidden @ self.token_embedding.float().t()
def forward(
self,
input_ids: Tensor,
*,
past_key_values: tuple[tuple[Tensor, Tensor], ...] | None = None,
attention_mask: Tensor | None = None,
use_cache: bool = False,
) -> tuple[Tensor, tuple[tuple[Tensor, Tensor], ...] | None]:
if input_ids.ndim != 2 or input_ids.shape[1] == 0:
raise ValueError("input_ids must have shape (batch, non-empty sequence)")
batch, length = input_ids.shape
if past_key_values is not None and len(past_key_values) != EFFECTIVE_DEPTH:
raise ValueError("past_key_values must contain one cache per effective depth")
past_length = 0 if past_key_values is None else past_key_values[0][0].shape[2]
if past_length + length > CONTEXT_LENGTH:
raise ValueError(f"sequence exceeds context_length={CONTEXT_LENGTH}")
positions = torch.arange(past_length, past_length + length, device=input_ids.device)
hidden = self._embed(input_ids)
caches: list[tuple[Tensor, Tensor]] = []
for depth, physical_index in enumerate(EXECUTION_ORDER):
hidden = hidden + self.depth_embeddings[depth].view(1, 1, -1)
block = cast(PhysicalBlock, self.blocks[physical_index])
attention_input = self.attention_norms[depth](hidden)
past = None if past_key_values is None else past_key_values[depth]
attended, cache = block.attention(attention_input, positions, past, attention_mask)
hidden = hidden + self.attention_residual_scales[depth] * attended
mlp_input = self.mlp_norms[depth](hidden)
mlp_output = block.mlp_down(
torch.nn.functional.silu(block.mlp_gate(mlp_input)) * block.mlp_up(mlp_input)
)
hidden = hidden + self.mlp_residual_scales[depth] * mlp_output
caches.append(cache)
logits = self._logits(self.final_norm(hidden))
return logits, tuple(caches) if use_cache else None
def _expected_shapes(model: nn.Module) -> dict[str, tuple[int, ...]]:
return {name: tuple(parameter.shape) for name, parameter in model.named_parameters()}
def load_model(checkpoint: str | Path, device: str = "cpu") -> FleckModel:
"""Load the public BF16 artifact strictly, then run it as FP32."""
if device not in {"cpu", "mps"}:
raise ValueError("device must be 'cpu' or 'mps'")
if device == "mps" and not torch.backends.mps.is_available():
raise RuntimeError("MPS was requested but is not available")
source = Path(checkpoint)
if not source.is_file():
raise FileNotFoundError(source)
model = FleckModel()
expected = _expected_shapes(model)
with safe_open(str(source), framework="pt", device="cpu") as handle:
metadata = handle.metadata() or {}
for key, expected_value in MODEL_METADATA.items():
if metadata.get(key) != expected_value:
raise ValueError(
f"SafeTensors metadata mismatch for {key}: "
f"expected {expected_value!r}, got {metadata.get(key)!r}"
)
names = set(handle.keys())
if names != set(expected):
raise ValueError(
"SafeTensors parameter names mismatch: "
f"missing={sorted(set(expected) - names)}, extra={sorted(names - set(expected))}"
)
for name, shape in expected.items():
tensor_slice = handle.get_slice(name)
if tensor_slice.get_dtype() != "BF16":
raise ValueError(f"{name}: expected BF16, got {tensor_slice.get_dtype()}")
if tuple(tensor_slice.get_shape()) != shape:
raise ValueError(
f"{name}: expected shape {shape}, got {tuple(tensor_slice.get_shape())}"
)
tensors = load_file(str(source), device="cpu")
for name, shape in expected.items():
tensor = tensors[name]
if tensor.dtype != torch.bfloat16 or tuple(tensor.shape) != shape:
raise ValueError(f"{name}: SafeTensors dtype/shape contract mismatch")
model.load_state_dict(tensors, strict=True, assign=True)
model = model.to(device=torch.device(device), dtype=torch.bfloat16).eval()
if any(parameter.dtype != torch.bfloat16 for parameter in model.parameters()):
raise TypeError("model parameters must be BF16 after loading")
return model
def load_tokenizer(path: str | Path) -> Tokenizer:
source = Path(path)
tokenizer = Tokenizer.from_file(str(source))
if tokenizer.get_vocab_size() != VOCAB_SIZE:
raise ValueError(f"tokenizer vocabulary must be {VOCAB_SIZE}")
expected = {
"<bos>": BOS_ID,
"<eos>": EOS_ID,
"<pad>": PAD_ID,
"<unk>": UNK_ID,
"<|system|>": SYSTEM_ID,
"<|user|>": USER_ID,
"<|assistant|>": ASSISTANT_ID,
"<|eot|>": EOT_ID,
}
if set(tokenizer.get_added_tokens_decoder()) != set(expected.values()):
raise ValueError("tokenizer added-token set does not match the strict contract")
for token, token_id in expected.items():
added_token = tokenizer.get_added_tokens_decoder().get(token_id)
if (
tokenizer.token_to_id(token) != token_id
or added_token is None
or getattr(added_token, "content", None) != token
or not bool(getattr(added_token, "special", False))
):
raise ValueError(f"tokenizer special token contract mismatch for {token}")
return tokenizer
@torch.inference_mode()
def generate(
model: FleckModel,
input_ids: Tensor,
max_tokens: int,
stop_token_ids: Iterable[int] = (EOS_ID,),
) -> Tensor:
if max_tokens < 0:
raise ValueError("max_tokens must be non-negative")
if input_ids.shape[1] + max_tokens > CONTEXT_LENGTH:
raise ValueError(f"prompt plus generation exceeds context_length={CONTEXT_LENGTH}")
stop_ids = set(stop_token_ids)
generated = input_ids
cache: tuple[tuple[Tensor, Tensor], ...] | None = None
for _ in range(max_tokens):
current = generated if cache is None else generated[:, -1:]
logits, cache = model(current, past_key_values=cache, use_cache=True)
next_token = logits[:, -1, :].argmax(dim=-1, keepdim=True)
generated = torch.cat((generated, next_token), dim=1)
if bool(torch.all(torch.isin(next_token, torch.tensor(list(stop_ids), device=next_token.device)))):
break
return generated
def _default_path(name: str) -> str:
return str(Path(__file__).with_name(name))
def chat_ids(tokenizer: Tokenizer, user_text: str) -> list[int]:
"""Encode one training-format user turn and open the assistant turn."""
return [BOS_ID, USER_ID, *tokenizer.encode(user_text, add_special_tokens=False).ids, EOT_ID, ASSISTANT_ID]
def decode_reply(tokenizer: Tokenizer, token_ids: list[int]) -> str:
"""Stop at EOT/EOS and decode only ordinary text."""
for index, token_id in enumerate(token_ids):
if token_id in {EOT_ID, EOS_ID}:
token_ids = token_ids[:index]
break
return tokenizer.decode(token_ids, skip_special_tokens=True).strip()
def run_single(args: argparse.Namespace, tokenizer: Tokenizer, model: FleckModel) -> None:
prompt = args.prompt if args.prompt is not None else "Hello"
ids = chat_ids(tokenizer, prompt)
input_ids = torch.tensor([ids], dtype=torch.long, device=args.device)
output = generate(model, input_ids, args.max_tokens, (EOT_ID, EOS_ID))
reply = decode_reply(tokenizer, output[0, len(ids) :].detach().cpu().tolist())
print(reply)
def run_chat(args: argparse.Namespace, tokenizer: Tokenizer, model: FleckModel) -> None:
history: list[tuple[str, str]] = []
print("Fleck chat. Type /exit to quit or /clear to reset history.")
while True:
try:
prompt = input("user> ")
except (EOFError, KeyboardInterrupt):
print()
return
if prompt.strip() == "/exit":
return
if prompt.strip() == "/clear":
history.clear()
print("(history cleared)")
continue
if not prompt.strip():
continue
ids = [BOS_ID]
for old_user, old_reply in history:
ids.extend([USER_ID, *tokenizer.encode(old_user, add_special_tokens=False).ids, EOT_ID, ASSISTANT_ID])
ids.extend([*tokenizer.encode(old_reply, add_special_tokens=False).ids, EOT_ID])
ids.extend([USER_ID, *tokenizer.encode(prompt, add_special_tokens=False).ids, EOT_ID, ASSISTANT_ID])
if len(ids) + args.max_tokens > CONTEXT_LENGTH:
history.clear()
ids = chat_ids(tokenizer, prompt)
input_ids = torch.tensor([ids], dtype=torch.long, device=args.device)
output = generate(model, input_ids, args.max_tokens, (EOT_ID, EOS_ID))
reply = decode_reply(tokenizer, output[0, len(ids) :].detach().cpu().tolist())
print(f"assistant> {reply}")
history.append((prompt, reply))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ckpt", default=_default_path("model.safetensors"))
parser.add_argument("--tokenizer", default=_default_path("tokenizer.json"))
parser.add_argument("--prompt", default=None, help="single prompt; omit for interactive chat")
parser.add_argument("--max-tokens", type=int, default=32)
parser.add_argument("--device", choices=("cpu", "mps"), default="cpu")
parser.add_argument("--no-chat", action="store_true", help="run one prompt instead of interactive chat")
return parser.parse_args()
def main() -> None:
args = parse_args()
tokenizer = load_tokenizer(args.tokenizer)
model = load_model(args.ckpt, args.device)
if args.no_chat or args.prompt is not None:
run_single(args, tokenizer, model)
else:
run_chat(args, tokenizer, model)
if __name__ == "__main__":
main()