gnumanth commited on
Commit
c5fcd3f
·
verified ·
1 Parent(s): 06c2bf8

Upload fine-tuned ScribeLMForAuthoring (71.1M PyTorch FP32 + 95.7 MB Int8 ONNX)

Browse files
README.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: transformers
4
+ tags:
5
+ - scribelm
6
+ - style-transfer
7
+ - technical-writing
8
+ - onnx
9
+ - smollm2
10
+ datasets:
11
+ - gnumanth/scribelm-dataset
12
+ ---
13
+
14
+ # `gnumanth/scribelm` — 71.1M Multi-Head Authoring & Voice Critic Model (`PyTorch FP32` + `ONNX QUInt8`)
15
+
16
+ Fine-tuned on **388 Markdown blog posts (`~230,000` tokens)** and social threads authored by **Hemanth HM** (`h3manth.com/scribe`).
17
+
18
+ ## Architecture (`ScribeLMForAuthoring`, `71.13M` params)
19
+ - **Backbone**: 12-layer `SmolLM2` causal transformer (`d=576`, `vocab=49,152`, eager attention for WASM SIMD compatibility).
20
+ - **4 Joint Heads**:
21
+ 1. `lm_head`: Causal text generation (`blog`, `rewrite`, `social`, `critic`).
22
+ 2. `voice_head`: Calibrated `p_hemanth` authenticity probability (`AUROC = 0.9988`).
23
+ 3. `style_head`: 5-axis cadence telemetry (`directness`, `hook_strength`, `zero_fluff`, `code_first`, `cadence`).
24
+ 4. `task_head`: 4-way writing task router (`100.0%` held-out accuracy).
25
+
26
+ ## Artifacts Included
27
+ - `model.safetensors`: `271.35 MB` PyTorch FP32 weights
28
+ - `onnx/model_quantized.onnx`: `95.71 MB` (`3.97x` smaller) `QUInt8` dynamically quantized ONNX model for `onnxruntime-web` (WASM SIMD) and CPU edge execution
calibration_metrics.json ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_id": "gnumanth/scribelm",
3
+ "parameters": 71129674,
4
+ "training_seconds": 386.42,
5
+ "epochs": 2,
6
+ "final_validation": {
7
+ "val_lm_loss": 4.8837,
8
+ "val_perplexity": 132.1137,
9
+ "val_task_accuracy": 1.0,
10
+ "val_style_mae": 0.1604,
11
+ "voice_auroc": 1.0,
12
+ "voice_ece": 0.1072,
13
+ "voice_brier": 0.0374,
14
+ "epoch": 2,
15
+ "train_loss": 4.7738
16
+ },
17
+ "history": [
18
+ {
19
+ "val_lm_loss": 5.1513,
20
+ "val_perplexity": 172.6528,
21
+ "val_task_accuracy": 1.0,
22
+ "val_style_mae": 0.1693,
23
+ "voice_auroc": 1.0,
24
+ "voice_ece": 0.0963,
25
+ "voice_brier": 0.0502,
26
+ "epoch": 1,
27
+ "train_loss": 7.2724
28
+ },
29
+ {
30
+ "val_lm_loss": 4.8837,
31
+ "val_perplexity": 132.1137,
32
+ "val_task_accuracy": 1.0,
33
+ "val_style_mae": 0.1604,
34
+ "voice_auroc": 1.0,
35
+ "voice_ece": 0.1072,
36
+ "voice_brier": 0.0374,
37
+ "epoch": 2,
38
+ "train_loss": 4.7738
39
+ }
40
+ ]
41
+ }
chat_template.jinja ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system
2
+ You are a helpful AI assistant named SmolLM, trained by Hugging Face<|im_end|>
3
+ ' }}{% endif %}{{'<|im_start|>' + message['role'] + '
4
+ ' + message['content'] + '<|im_end|>' + '
5
+ '}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant
6
+ ' }}{% endif %}
config.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "ScribeLMForAuthoring"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "modeling_scribelm.ScribeLMConfig",
7
+ "AutoModel": "modeling_scribelm.ScribeLMForAuthoring"
8
+ },
9
+ "base_model_name": "HuggingFaceTB/SmolLM2-135M-Instruct",
10
+ "dropout": 0.1,
11
+ "dtype": "float32",
12
+ "hidden_act": "silu",
13
+ "hidden_size": 576,
14
+ "intermediate_size": 1536,
15
+ "layer_indices": [
16
+ 0,
17
+ 1,
18
+ 2,
19
+ 3,
20
+ 9,
21
+ 12,
22
+ 15,
23
+ 18,
24
+ 26,
25
+ 27,
26
+ 28,
27
+ 29
28
+ ],
29
+ "max_position_embeddings": 2048,
30
+ "model_type": "scribelm",
31
+ "num_attention_heads": 9,
32
+ "num_hidden_layers": 12,
33
+ "num_key_value_heads": 3,
34
+ "num_style_axes": 5,
35
+ "num_tasks": 4,
36
+ "rms_norm_eps": 1e-05,
37
+ "rope_theta": 100000.0,
38
+ "transformers_version": "5.12.1",
39
+ "vocab_size": 49152
40
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:879e8e752524fbbb5366e89837905c7c0afb026e28314029bfc354c6cd2d0a51
3
+ size 284532904
modeling_scribelm.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom Hugging Face PreTrainedConfig and PreTrainedModel for `scribelm`.
3
+
4
+ Architecture: `ScribeLMForAuthoring` (~70.8M parameters)
5
+ - Backbone: 12-layer Llama/SmolLM2 Causal Transformer (`hidden_size=576`, `num_attention_heads=9`,
6
+ `num_key_value_heads=3`, `intermediate_size=1536`, `vocab_size=49152`) initialized from
7
+ `HuggingFaceTB/SmolLM2-135M-Instruct`.
8
+ - Joint Multi-Task Heads:
9
+ 1. `lm_head`: Causal language modeling head generating Hemanth HM blog posts, rewrites, and social threads.
10
+ 2. `voice_head`: Calibrated `p_hemanth` logit scoring authentic practitioner voice vs corporate/LLM fluff.
11
+ 3. `style_head`: 5-axis style telemetry head predicting `(directness, hook_strength, zero_fluff, code_first, cadence)`.
12
+ 4. `task_head`: 4-way writing task router `(blog, rewrite, social, critic)`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+ from transformers import AutoConfig, AutoModelForCausalLM, PretrainedConfig, PreTrainedModel
23
+
24
+
25
+ TASK_NAMES = ["blog", "rewrite", "social", "critic"]
26
+ TASK_TO_ID = {name: i for i, name in enumerate(TASK_NAMES)}
27
+ STYLE_AXES = ["directness", "hook_strength", "zero_fluff", "code_first", "cadence"]
28
+
29
+
30
+ class ScribeLMConfig(PretrainedConfig):
31
+ model_type = "scribelm"
32
+
33
+ def __init__(
34
+ self,
35
+ base_model_name: str = "HuggingFaceTB/SmolLM2-135M-Instruct",
36
+ vocab_size: int = 49152,
37
+ hidden_size: int = 576,
38
+ intermediate_size: int = 1536,
39
+ num_hidden_layers: int = 12,
40
+ num_attention_heads: int = 9,
41
+ num_key_value_heads: int = 3,
42
+ hidden_act: str = "silu",
43
+ max_position_embeddings: int = 2048,
44
+ rms_norm_eps: float = 1e-5,
45
+ rope_theta: float = 100000.0,
46
+ num_tasks: int = 4,
47
+ num_style_axes: int = 5,
48
+ dropout: float = 0.1,
49
+ layer_indices: Optional[List[int]] = None,
50
+ **kwargs: Any,
51
+ ) -> None:
52
+ super().__init__(**kwargs)
53
+ self.base_model_name = base_model_name
54
+ self.vocab_size = vocab_size
55
+ self.hidden_size = hidden_size
56
+ self.intermediate_size = intermediate_size
57
+ self.num_hidden_layers = num_hidden_layers
58
+ self.num_attention_heads = num_attention_heads
59
+ self.num_key_value_heads = num_key_value_heads
60
+ self.hidden_act = hidden_act
61
+ self.max_position_embeddings = max_position_embeddings
62
+ self.rms_norm_eps = rms_norm_eps
63
+ self.rope_theta = rope_theta
64
+ self.num_tasks = num_tasks
65
+ self.num_style_axes = num_style_axes
66
+ self.dropout = dropout
67
+ self.layer_indices = layer_indices or [0, 1, 2, 3, 9, 12, 15, 18, 26, 27, 28, 29]
68
+
69
+
70
+ class ScribeLMForAuthoring(PreTrainedModel):
71
+ config_class = ScribeLMConfig
72
+ _tied_weights_keys = ["causal_lm.lm_head.weight"]
73
+ all_tied_weights_keys = {"causal_lm.lm_head.weight": "causal_lm.model.embed_tokens.weight"}
74
+
75
+ def __init__(self, config: ScribeLMConfig, preload_pretrained: bool = False) -> None:
76
+ super().__init__(config)
77
+ self.config = config
78
+ self.all_tied_weights_keys = {"causal_lm.lm_head.weight": "causal_lm.model.embed_tokens.weight"}
79
+
80
+ llama_cfg = AutoConfig.from_pretrained(config.base_model_name)
81
+ llama_cfg.num_hidden_layers = config.num_hidden_layers
82
+ llama_cfg._attn_implementation = "eager"
83
+
84
+ if preload_pretrained:
85
+ full_lm = AutoModelForCausalLM.from_pretrained(
86
+ config.base_model_name,
87
+ dtype=torch.float32,
88
+ )
89
+ full_lm.config._attn_implementation = "eager"
90
+ keep = config.layer_indices[: config.num_hidden_layers]
91
+ full_lm.model.layers = nn.ModuleList([full_lm.model.layers[i] for i in keep])
92
+ full_lm.config.num_hidden_layers = config.num_hidden_layers
93
+ self.causal_lm = full_lm
94
+ else:
95
+ llama_cfg.torch_dtype = torch.float32
96
+ self.causal_lm = AutoModelForCausalLM.from_config(llama_cfg).float()
97
+
98
+ self.causal_lm = self.causal_lm.float()
99
+ h = config.hidden_size
100
+ self.dropout = nn.Dropout(config.dropout)
101
+ self.voice_head = nn.Sequential(
102
+ nn.Linear(h, h // 2),
103
+ nn.SiLU(),
104
+ nn.Linear(h // 2, 1),
105
+ )
106
+ self.style_head = nn.Sequential(
107
+ nn.Linear(h, h // 2),
108
+ nn.SiLU(),
109
+ nn.Linear(h // 2, config.num_style_axes),
110
+ )
111
+ self.task_head = nn.Linear(h, config.num_tasks)
112
+ self.causal_lm.lm_head.weight = self.causal_lm.model.embed_tokens.weight
113
+
114
+ def tie_weights(self, **kwargs: Any) -> None:
115
+ self.causal_lm.lm_head.weight = self.causal_lm.model.embed_tokens.weight
116
+
117
+ def forward(
118
+ self,
119
+ input_ids: torch.Tensor,
120
+ attention_mask: Optional[torch.Tensor] = None,
121
+ labels: Optional[torch.Tensor] = None,
122
+ voice_labels: Optional[torch.Tensor] = None,
123
+ style_labels: Optional[torch.Tensor] = None,
124
+ task_labels: Optional[torch.Tensor] = None,
125
+ ) -> Dict[str, torch.Tensor]:
126
+ if attention_mask is None:
127
+ attention_mask = torch.ones_like(input_ids)
128
+
129
+ out = self.causal_lm(
130
+ input_ids=input_ids,
131
+ attention_mask=attention_mask,
132
+ use_cache=False,
133
+ output_hidden_states=True,
134
+ )
135
+ logits = out.logits
136
+ last_hidden = out.hidden_states[-1]
137
+
138
+ mask_f = attention_mask.unsqueeze(-1).to(last_hidden.dtype)
139
+ pooled = (last_hidden * mask_f).sum(dim=1) / mask_f.sum(dim=1).clamp(min=1.0)
140
+ pooled_drop = self.dropout(pooled)
141
+
142
+ voice_logit = self.voice_head(pooled_drop).squeeze(-1)
143
+ style_pred = torch.sigmoid(self.style_head(pooled_drop))
144
+ task_logits = self.task_head(pooled_drop)
145
+
146
+ result: Dict[str, torch.Tensor] = {
147
+ "logits": logits,
148
+ "next_token_logits": logits[:, -1, :],
149
+ "voice_logit": voice_logit,
150
+ "p_hemanth": torch.sigmoid(voice_logit),
151
+ "style_scores": style_pred,
152
+ "task_logits": task_logits,
153
+ }
154
+
155
+ total_loss = None
156
+ if labels is not None:
157
+ shift_logits = logits[:, :-1, :].contiguous()
158
+ shift_labels = labels[:, 1:].contiguous()
159
+ lm_loss = F.cross_entropy(
160
+ shift_logits.view(-1, shift_logits.size(-1)),
161
+ shift_labels.view(-1),
162
+ ignore_index=-100,
163
+ )
164
+ result["lm_loss"] = lm_loss
165
+ total_loss = lm_loss
166
+
167
+ if voice_labels is not None:
168
+ v_loss = F.binary_cross_entropy_with_logits(
169
+ voice_logit.view(-1),
170
+ voice_labels.view(-1).to(voice_logit.dtype),
171
+ )
172
+ result["voice_loss"] = v_loss
173
+ total_loss = v_loss if total_loss is None else total_loss + 0.35 * v_loss
174
+
175
+ if style_labels is not None:
176
+ s_loss = F.smooth_l1_loss(style_pred, style_labels.to(style_pred.dtype))
177
+ result["style_loss"] = s_loss
178
+ total_loss = s_loss if total_loss is None else total_loss + 0.45 * s_loss
179
+
180
+ if task_labels is not None:
181
+ t_loss = F.cross_entropy(task_logits, task_labels.view(-1).long())
182
+ result["task_loss"] = t_loss
183
+ total_loss = t_loss if total_loss is None else total_loss + 0.20 * t_loss
184
+
185
+ if total_loss is not None:
186
+ result["loss"] = total_loss
187
+
188
+ return result
189
+
190
+ @torch.no_grad()
191
+ def generate_text(
192
+ self,
193
+ tokenizer: Any,
194
+ prompt: str,
195
+ max_new_tokens: int = 96,
196
+ temperature: float = 0.25,
197
+ top_p: float = 0.9,
198
+ repetition_penalty: float = 1.15,
199
+ ) -> Dict[str, Any]:
200
+ self.eval()
201
+ device = next(self.parameters()).device
202
+ enc = tokenizer(
203
+ prompt,
204
+ return_tensors="pt",
205
+ truncation=True,
206
+ max_length=256,
207
+ )
208
+ input_ids = enc["input_ids"].to(device)
209
+ attention_mask = enc["attention_mask"].to(device)
210
+
211
+ first_out = self.forward(input_ids=input_ids, attention_mask=attention_mask)
212
+ p_hemanth = float(first_out["p_hemanth"][0].item())
213
+ style_vec = [round(float(x), 4) for x in first_out["style_scores"][0].tolist()]
214
+ task_probs = torch.softmax(first_out["task_logits"][0], dim=-1).tolist()
215
+ pred_task_idx = int(torch.argmax(first_out["task_logits"][0]).item())
216
+
217
+ generated: List[int] = []
218
+ cur_ids = input_ids
219
+ cur_mask = attention_mask
220
+
221
+ eos_id = tokenizer.eos_token_id
222
+ for step in range(max_new_tokens):
223
+ if step == 0:
224
+ next_logits = first_out["next_token_logits"][0].clone()
225
+ else:
226
+ step_out = self.causal_lm(
227
+ input_ids=cur_ids,
228
+ attention_mask=cur_mask,
229
+ use_cache=False,
230
+ )
231
+ next_logits = step_out.logits[0, -1, :].clone()
232
+
233
+ # Repetition penalty over recently generated tokens
234
+ for seen_id in set(generated[-48:]):
235
+ if next_logits[seen_id] > 0:
236
+ next_logits[seen_id] /= repetition_penalty
237
+ else:
238
+ next_logits[seen_id] *= repetition_penalty
239
+
240
+ if temperature <= 1e-4:
241
+ next_token = int(torch.argmax(next_logits).item())
242
+ else:
243
+ scaled = next_logits / temperature
244
+ sorted_logits, sorted_indices = torch.sort(scaled, descending=True)
245
+ cum_probs = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1)
246
+ remove_mask = cum_probs > top_p
247
+ remove_mask[..., 1:] = remove_mask[..., :-1].clone()
248
+ remove_mask[..., 0] = False
249
+ sorted_logits[remove_mask] = -float("inf")
250
+ probs = torch.softmax(sorted_logits, dim=-1)
251
+ sampled_idx = int(torch.multinomial(probs, num_samples=1).item())
252
+ next_token = int(sorted_indices[sampled_idx].item())
253
+
254
+ if eos_id is not None and next_token == eos_id and len(generated) >= 8:
255
+ break
256
+ generated.append(next_token)
257
+ next_t = torch.tensor([[next_token]], dtype=torch.long, device=device)
258
+ cur_ids = torch.cat([cur_ids, next_t], dim=1)
259
+ cur_mask = torch.cat([cur_mask, torch.ones_like(next_t)], dim=1)
260
+
261
+ text_out = tokenizer.decode(generated, skip_special_tokens=True).strip()
262
+ return {
263
+ "text": text_out,
264
+ "p_hemanth": round(p_hemanth, 4),
265
+ "predicted_task": TASK_NAMES[pred_task_idx],
266
+ "task_probs": {TASK_NAMES[i]: round(float(task_probs[i]), 4) for i in range(len(TASK_NAMES))},
267
+ "style_scores": {STYLE_AXES[i]: style_vec[i] for i in range(len(STYLE_AXES))},
268
+ }
onnx/model_quantized.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7cd3fef3858c7e5100336207d29524061e6c2b07f48dc49e8165aea44930eede
3
+ size 100360911
onnx/onnx_meta.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "fp32_mb": 379.86,
3
+ "int8_mb": 95.71,
4
+ "compression_ratio": 3.97
5
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<|im_start|>",
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "extra_special_tokens": [
9
+ "<|im_start|>",
10
+ "<|im_end|>"
11
+ ],
12
+ "is_local": false,
13
+ "local_files_only": false,
14
+ "model_max_length": 8192,
15
+ "pad_token": "<|im_end|>",
16
+ "tokenizer_class": "GPT2Tokenizer",
17
+ "unk_token": "<|endoftext|>",
18
+ "vocab_size": 49152
19
+ }