File size: 7,448 Bytes
70a25ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559972f
 
70a25ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"""Export instruction-tuned weights and a standalone CPU demo, without uploading.

    python export.py --checkpoint checkpoints/sft/best.pt --out checkpoints/release
"""

import argparse
import json
from pathlib import Path
import shutil
import subprocess

import torch
from safetensors.torch import save_model

from eval import load as load_checkpoint
from inference import FIELDS, TEMPLATE, digest, load


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--checkpoint", default="checkpoints/sft/best.pt")
    parser.add_argument("--data", default="data")
    parser.add_argument("--out", default="checkpoints/release")
    args = parser.parse_args()
    out = Path(args.out)
    if out.exists() and any(out.iterdir()):
        raise ValueError("Output directory is not empty. Use a new directory.")
    torch.set_num_threads(4)
    model, tok, cfg, step = load_checkpoint(args.checkpoint, args.data, "cpu")
    checkpoint = torch.load(args.checkpoint, map_location="cpu", weights_only=False)
    if "sft" not in checkpoint or checkpoint["sft"]["template"] != TEMPLATE:
        raise ValueError("Export requires an instruction-tuned checkpoint with the story template.")
    out.mkdir(parents=True, exist_ok=True)
    save_model(model, str(out / "model.safetensors"))
    shutil.copyfile(Path(args.data) / "tokenizer.model", out / "tokenizer.model")
    architecture = {key: cfg[key] for key in ("n_layer", "dim", "n_head", "ctx", "vocab")}
    architecture["pos"] = cfg.get("pos", "rope")
    root = Path(__file__).resolve().parent
    commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip()
    config = dict(format_version=1, model=architecture, template=TEMPLATE, fields=FIELDS,
                  special_tokens=tok.special, dtype="float32", step=step,
                  source_checkpoint_sha256=digest(args.checkpoint), source_commit=commit,
                  training_fingerprint=checkpoint["fingerprint"],
                  instruction_revision=checkpoint["sft"]["revision"],
                  sha256={name: digest(out / name) for name in ("model.safetensors", "tokenizer.model")})
    (out / "config.json").write_text(json.dumps(config, indent=2) + "\n")
    for name in ("model.py", "tokenizer.py", "inference.py", "app.py", "requirements.txt", "LICENSE"):
        shutil.copyfile(root / name, out / name)
    params = sum(p.numel() for p in model.parameters())
    run = checkpoint.get("wandb_id")
    card = f"""---
license: mit
language:
- en
datasets:
- roneneldan/TinyStories
- roneneldan/TinyStoriesInstruct
pipeline_tag: text-generation
tags:
- pytorch
- safetensors
- custom-code
---

# Sprout story model

A decoder-only model trained from scratch on TinyStoriesV2-GPT4 and instruction tuned on TinyStoriesInstruct. It has {params:,} parameters, {cfg['n_layer']} layers, width {cfg['dim']}, {cfg['n_head']} attention heads, a {cfg['ctx']}-token context, {architecture['pos']} positions, RMSNorm, SwiGLU, tied embeddings, and a {cfg['vocab']}-token byte-level BPE vocabulary.

[Try the live browser demo](https://huggingface.co/spaces/GeorgeCodesSometimes/sprout). The static demo runs float32 ONNX weights on your device; prompts stay local.

This bundle contains float32 inference weights at instruction step {step:,}. It excludes optimizer state, training data, credentials, and training dependencies. The tied embedding/output tensor is stored once and restored with `safetensors.torch.load_model`. `config.json` records weight and tokenizer SHA-256 checksums, source checkpoint SHA-256, source code commit, and the original training fingerprint.

## Run locally

Download all files from this model repository into an empty directory, then run:

```bash
python -m pip install -r requirements.txt
python app.py
```

Use the included loading module directly:

```python
from inference import load, stream

model, tok, config = load(".")
for text in stream(model, tok, {{"Summary": "A little fox learns to be patient."}}):
    print(text, end="\\r", flush=True)
```

`load` also accepts a Hub model ID and an optional `revision` commit. The included code is the loading contract; this is not a Transformers `AutoModel` repository. Loading does not require any training corpus files. The demo uses the exact training template, keeps the full instruction in context, and supports streamed output, stopping, temperature, and top-k.

## Training and evaluation

The source checkpoint's instruction validation loss is {checkpoint['best_val']:.6f}. The [training run](https://wandb.ai/george-nasseem/sprout/runs/{run}) records configuration and losses. See the [project README](https://github.com/gnasseem/sprout#instruction-tuning) for the fixed behavioral evaluation, comparison with the pretrained model, and base-validation regression. Low validation loss does not establish reliable constraint following.

Pretraining uses the story-level seeded split in the project, with 537,686,596 training tokens and 5,402,089 validation tokens. Instruction preparation excludes malformed and duplicate stories, held-out story overlap, and conversations beyond the context limit. TinyStoriesInstruct is pinned to `{config['instruction_revision']}`. These are synthetic English stories generated by GPT models, not a general conversational corpus.

## Limitations and intended use

An educational story-generation experiment. It often misses requested words and sentences, repeats itself, and loses narrative details. Instruction tuning regresses base-story validation loss. Long instructions leave less room for the story. It has no general chat or factual-answering capability, no content filter, and no suitability evaluation for children. Outputs can contain biases or disturbing story content from its synthetic source corpus.

## License and provenance

Code and exported weights use the included MIT license. The upstream [TinyStories dataset card](https://huggingface.co/datasets/roneneldan/TinyStories) lists CDLA-Sharing-1.0; [TinyStoriesInstruct](https://huggingface.co/datasets/roneneldan/TinyStoriesInstruct) is the separate instruction corpus. No source stories are redistributed in this bundle. The dataset creators' [paper](https://arxiv.org/abs/2305.07759) describes TinyStories.
"""
    (out / "README.md").write_text(card, encoding="utf-8")
    (out / "SPACE.md").write_text("""---
title: Sprout Story Workshop
sdk: gradio
sdk_version: 6.30.0
python_version: '3.11'
app_file: app.py
pinned: false
license: mit
---

# Sprout Story Workshop

An experimental small story model. It can miss constraints, repeat itself, and lose story details.
""")
    restored, restored_tok, _ = load(out)
    prefix = tok.encode(TEMPLATE.format(instruction="Write a short story.\nSummary: A fox finds a seed."))
    x = torch.tensor([prefix])
    before = model.generate(x, 64, temperature=0)
    after = restored.generate(x, 64, temperature=0)
    assert torch.equal(before, after), "export changed greedy generation"
    assert model.head.weight is model.tok_emb.weight
    assert restored.head.weight is restored.tok_emb.weight
    assert restored_tok.decode(after[0].tolist()) == tok.decode(before[0].tolist())
    print(f"Exported {params:,} parameters; 64 greedy tokens match; tied weights and tokenizer match.")
    print(f"Bundle: {out.resolve()}; weights: {(out / 'model.safetensors').stat().st_size:,} bytes")


if __name__ == "__main__":
    main()