udaygs210 commited on
Commit
e7aef18
Β·
1 Parent(s): 196c27d

Add data transformer Gradio demo

Browse files
Files changed (4) hide show
  1. README.md +36 -7
  2. app.py +139 -0
  3. model.py +186 -0
  4. requirements.txt +5 -0
README.md CHANGED
@@ -1,13 +1,42 @@
1
  ---
2
- title: SLM Transform
3
- emoji: πŸ‘
4
- colorFrom: purple
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: SLM 264M Data Transformer
3
+ emoji: πŸ”„
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 4.44.0
 
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # SLM-264M Data Transformer β€” demo Space
13
+
14
+ A Gradio front-end for the `slm-264m` data-transformer fine-tune: convert data between
15
+ **CSV, JSON, and XML** with a 264M-parameter model trained from scratch, running on CPU.
16
+
17
+ ## How it's wired
18
+
19
+ - **Code lives here** (`app.py`, `model.py`).
20
+ - **Weights live in the model repo** and are downloaded at startup with
21
+ `huggingface_hub.hf_hub_download`. Set the repo in `app.py`:
22
+ ```python
23
+ MODEL_REPO = "uday210/slm-264m-transform" # your fine-tune
24
+ CKPT_FILENAME = "slm-264m-transform.pt"
25
+ TOKENIZER_FILENAME = "tokenizer.json"
26
+ ```
27
+ - If the model repo is **private**, add an `HF_TOKEN` secret under
28
+ *Settings β†’ Variables and secrets*.
29
+
30
+ ## Files you must add
31
+
32
+ 1. **`model.py`** β€” copy it from your training repo. This Space cannot load the checkpoint
33
+ without your exact `GPT`/`GPTConfig` classes (custom GQA + RoPE architecture).
34
+ It must be import-safe (no training at file top level).
35
+ 2. Make sure the model repo actually contains the checkpoint and `tokenizer.json`.
36
+
37
+ ## Notes / limits
38
+
39
+ - CPU inference: a few tok/s. Fine for a demo. For speed, upgrade the Space hardware or
40
+ wire in the KV-cache generation path from your deployment notes.
41
+ - The prompt template in `app.py` must match how the fine-tune was trained.
42
+ - Numeric transcription is the model's known weak spot; structure and text are reliable.
app.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio demo for the SLM-264M data transformer fine-tune.
3
+
4
+ Pattern: code lives here (in the Space), weights live in the model repo and are
5
+ pulled at startup with huggingface_hub. You bundle model.py alongside this file.
6
+
7
+ Files this Space expects:
8
+ - app.py (this file)
9
+ - model.py (YOUR architecture file β€” copy it from your training repo)
10
+ - requirements.txt
11
+ - README.md (Space card, has the sdk: gradio frontmatter)
12
+
13
+ Weights + tokenizer are downloaded from MODEL_REPO below. If that repo is private,
14
+ add an HF_TOKEN secret to the Space (Settings -> Variables and secrets).
15
+ """
16
+
17
+ import os
18
+ import torch
19
+ import gradio as gr
20
+ from tokenizers import Tokenizer
21
+ from huggingface_hub import hf_hub_download
22
+
23
+ # ---- point these at your fine-tuned transform model repo -------------------
24
+ # MODEL_REPO = os.environ.get("MODEL_REPO", "uday210/slm-264m-transform") # <-- EDIT
25
+ # CKPT_FILENAME = os.environ.get("CKPT_FILENAME", "slm-264m-transform.pt") # <-- EDIT
26
+ MODEL_REPO = "uday210/slm-transform-264m" # <-- your actual transform model repo
27
+ CKPT_FILENAME = "slm-transform-final.pt" # <-- the real checkpoint filename
28
+ TOKENIZER_FILENAME = os.environ.get("TOKENIZER_FILENAME", "tokenizer.json")
29
+
30
+ # ---- prompt template: MUST match how the fine-tune was trained -------------
31
+ # Your notes show the inference format as:
32
+ # "Convert this CSV to JSON:\n<data>\nOutput:\n"
33
+ # If your training pairs used a different wording, change it here or the model
34
+ # will be prompted off-distribution.
35
+ PROMPT_TEMPLATE = "Convert this {src} to {tgt}:\n{data}\nOutput:\n"
36
+
37
+ BLOCK_SIZE = 2048
38
+ EOT_TOKEN = "<|endoftext|>"
39
+
40
+ device = "cuda" if torch.cuda.is_available() else "cpu"
41
+ if device == "cpu":
42
+ torch.set_num_threads(os.cpu_count() or 4)
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Load architecture + weights
46
+ # ---------------------------------------------------------------------------
47
+ from model import GPT # noqa: E402 (your import-safe model.py)
48
+
49
+ _token = os.environ.get("HF_TOKEN") # only needed if the model repo is private
50
+
51
+ ckpt_path = hf_hub_download(MODEL_REPO, CKPT_FILENAME, token=_token)
52
+ tok_path = hf_hub_download(MODEL_REPO, TOKENIZER_FILENAME, token=_token)
53
+
54
+ ckpt = torch.load(ckpt_path, map_location=device, weights_only=False)
55
+ model = GPT(ckpt["config"])
56
+ model.load_state_dict(ckpt["model"])
57
+ model.eval().to(device)
58
+
59
+ tok = Tokenizer.from_file(tok_path)
60
+ EOT_ID = tok.token_to_id(EOT_TOKEN)
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Generation (greedy by default β€” this task is deterministic transformation)
65
+ # ---------------------------------------------------------------------------
66
+ @torch.no_grad()
67
+ def generate(prompt: str, max_new_tokens: int = 512, temperature: float = 0.0):
68
+ ids = tok.encode(prompt).ids
69
+ x = torch.tensor(ids, dtype=torch.long, device=device)[None]
70
+ start = len(ids)
71
+
72
+ for _ in range(max_new_tokens):
73
+ logits, _ = model(x[:, -BLOCK_SIZE:])
74
+ logits = logits[:, -1, :]
75
+ if temperature and temperature > 0:
76
+ probs = torch.softmax(logits / temperature, dim=-1)
77
+ nxt = torch.multinomial(probs, num_samples=1)
78
+ else:
79
+ nxt = logits.argmax(dim=-1, keepdim=True)
80
+ x = torch.cat([x, nxt], dim=1)
81
+ if EOT_ID is not None and nxt.item() == EOT_ID:
82
+ break
83
+ # stream the decoded suffix so far
84
+ yield tok.decode(x[0, start:].tolist())
85
+
86
+ yield tok.decode(x[0, start:].tolist())
87
+
88
+
89
+ def transform(data, src_fmt, tgt_fmt, max_new_tokens, temperature):
90
+ data = (data or "").strip()
91
+ if not data:
92
+ yield "Paste some input data first."
93
+ return
94
+ if src_fmt == tgt_fmt:
95
+ yield "Source and target format are the same β€” pick different formats."
96
+ return
97
+ prompt = PROMPT_TEMPLATE.format(src=src_fmt, tgt=tgt_fmt, data=data)
98
+ for partial in generate(prompt, int(max_new_tokens), float(temperature)):
99
+ yield partial
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # UI
104
+ # ---------------------------------------------------------------------------
105
+ FORMATS = ["CSV", "JSON", "XML"]
106
+
107
+ EXAMPLE_CSV = "name,city,age\nalice,london,31\nbob,paris,47\ncarol,tokyo,29"
108
+
109
+ with gr.Blocks(title="SLM-264M Data Transformer", theme=gr.themes.Soft()) as demo:
110
+ gr.Markdown(
111
+ "# πŸ”„ SLM-264M Data Transformer\n"
112
+ "A 264M-parameter small language model, trained from scratch and fine-tuned to "
113
+ "convert between **CSV, JSON, and XML**. Runs on CPU β€” expect a few tokens/sec.\n\n"
114
+ "*Note: numbers can occasionally be miscopied; text and structure are reliable.*"
115
+ )
116
+ with gr.Row():
117
+ with gr.Column():
118
+ data_in = gr.Textbox(
119
+ label="Input data", value=EXAMPLE_CSV, lines=8,
120
+ placeholder="Paste CSV, JSON, or XML here…",
121
+ )
122
+ with gr.Row():
123
+ src = gr.Dropdown(FORMATS, value="CSV", label="From")
124
+ tgt = gr.Dropdown(FORMATS, value="JSON", label="To")
125
+ with gr.Accordion("Advanced", open=False):
126
+ max_tok = gr.Slider(64, 1024, value=512, step=64, label="Max new tokens")
127
+ temp = gr.Slider(
128
+ 0.0, 1.0, value=0.0, step=0.1,
129
+ label="Temperature (0 = greedy, recommended for exact transforms)",
130
+ )
131
+ go = gr.Button("Transform", variant="primary")
132
+ with gr.Column():
133
+ out = gr.Textbox(label="Output", lines=14, show_copy_button=True)
134
+
135
+ go.click(transform, [data_in, src, tgt, max_tok, temp], out)
136
+ data_in.submit(transform, [data_in, src, tgt, max_tok, temp], out)
137
+
138
+ if __name__ == "__main__":
139
+ demo.launch()
model.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ model.py β€” Path B architecture for the ~250M SLM base.
3
+
4
+ Modifications from build-nanogpt's GPT:
5
+ 1. GQA (grouped-query attention) β€” fewer KV heads than query heads => small KV cache
6
+ 2. RoPE (rotary position embeddings) β€” replaces learned wpe; extendable context
7
+ 3. Tied embeddings β€” kept from baseline
8
+ 4. Vocab size is a config field (set it to your trained tokenizer's size, e.g. 32768)
9
+
10
+ Kept simple (LayerNorm + GELU) on purpose for a low-risk first real model.
11
+ This file only DEFINES the model β€” it never trains on import, so it's safe to
12
+ `from model import GPT, GPTConfig` from any script.
13
+ """
14
+ import math
15
+ from dataclasses import dataclass
16
+ import torch
17
+ import torch.nn as nn
18
+ from torch.nn import functional as F
19
+
20
+ # -----------------------------------------------------------------------------
21
+ # RoPE helpers
22
+
23
+ def build_rope_cache(seq_len, head_dim, device, base=10000.0):
24
+ """Precompute cos/sin tables for rotary embeddings. Shape: (seq_len, head_dim)."""
25
+ assert head_dim % 2 == 0, "head_dim must be even for RoPE"
26
+ theta = 1.0 / (base ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
27
+ positions = torch.arange(seq_len, device=device).float()
28
+ freqs = torch.outer(positions, theta) # (seq_len, head_dim/2)
29
+ emb = torch.cat([freqs, freqs], dim=-1) # (seq_len, head_dim)
30
+ return emb.cos(), emb.sin()
31
+
32
+ def rotate_half(x):
33
+ x1, x2 = x.chunk(2, dim=-1)
34
+ return torch.cat((-x2, x1), dim=-1)
35
+
36
+ def apply_rope(q, k, cos, sin):
37
+ # q, k: (B, n_head, T, head_dim); cos, sin: (T, head_dim)
38
+ cos = cos.unsqueeze(0).unsqueeze(0) # (1, 1, T, head_dim)
39
+ sin = sin.unsqueeze(0).unsqueeze(0)
40
+ q_rot = (q * cos) + (rotate_half(q) * sin)
41
+ k_rot = (k * cos) + (rotate_half(k) * sin)
42
+ return q_rot, k_rot
43
+
44
+ # -----------------------------------------------------------------------------
45
+ # Grouped-Query Attention with RoPE
46
+
47
+ class CausalSelfAttention(nn.Module):
48
+ def __init__(self, config):
49
+ super().__init__()
50
+ assert config.n_embd % config.n_head == 0
51
+ assert config.n_head % config.n_kv_head == 0, "n_head must be divisible by n_kv_head"
52
+ self.n_head = config.n_head
53
+ self.n_kv_head = config.n_kv_head
54
+ self.n_embd = config.n_embd
55
+ self.head_dim = config.n_embd // config.n_head
56
+ self.n_rep = self.n_head // self.n_kv_head # how many query heads share each KV head
57
+
58
+ # Q projects to full n_head; K and V project to only n_kv_head => smaller KV
59
+ self.q_proj = nn.Linear(config.n_embd, self.n_head * self.head_dim, bias=False)
60
+ self.k_proj = nn.Linear(config.n_embd, self.n_kv_head * self.head_dim, bias=False)
61
+ self.v_proj = nn.Linear(config.n_embd, self.n_kv_head * self.head_dim, bias=False)
62
+ self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
63
+ self.c_proj.NANOGPT_SCALE_INIT = 1
64
+
65
+ def forward(self, x, cos, sin):
66
+ B, T, C = x.size()
67
+ q = self.q_proj(x).view(B, T, self.n_head, self.head_dim).transpose(1, 2) # (B, nh, T, hd)
68
+ k = self.k_proj(x).view(B, T, self.n_kv_head, self.head_dim).transpose(1, 2) # (B, nkv, T, hd)
69
+ v = self.v_proj(x).view(B, T, self.n_kv_head, self.head_dim).transpose(1, 2) # (B, nkv, T, hd)
70
+
71
+ # apply rotary embeddings to q and k
72
+ q, k = apply_rope(q, k, cos[:T], sin[:T])
73
+
74
+ # expand KV heads to match query heads (GQA): repeat each KV head n_rep times
75
+ k = k.repeat_interleave(self.n_rep, dim=1) # (B, nh, T, hd)
76
+ v = v.repeat_interleave(self.n_rep, dim=1) # (B, nh, T, hd)
77
+
78
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True) # flash attention
79
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
80
+ return self.c_proj(y)
81
+
82
+ class MLP(nn.Module):
83
+ def __init__(self, config):
84
+ super().__init__()
85
+ self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=False)
86
+ self.gelu = nn.GELU(approximate='tanh')
87
+ self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=False)
88
+ self.c_proj.NANOGPT_SCALE_INIT = 1
89
+
90
+ def forward(self, x):
91
+ return self.c_proj(self.gelu(self.c_fc(x)))
92
+
93
+ class Block(nn.Module):
94
+ def __init__(self, config):
95
+ super().__init__()
96
+ self.ln_1 = nn.LayerNorm(config.n_embd)
97
+ self.attn = CausalSelfAttention(config)
98
+ self.ln_2 = nn.LayerNorm(config.n_embd)
99
+ self.mlp = MLP(config)
100
+
101
+ def forward(self, x, cos, sin):
102
+ x = x + self.attn(self.ln_1(x), cos, sin)
103
+ x = x + self.mlp(self.ln_2(x))
104
+ return x
105
+
106
+ # -----------------------------------------------------------------------------
107
+
108
+ @dataclass
109
+ class GPTConfig:
110
+ block_size: int = 2048 # context length
111
+ vocab_size: int = 32768 # SET to your trained tokenizer's size (incl. FIM/special tokens)
112
+ n_layer: int = 24
113
+ n_head: int = 16 # query heads
114
+ n_kv_head: int = 4 # KV heads (GQA); n_head/n_kv_head = 4 query heads per KV head
115
+ n_embd: int = 1024
116
+
117
+ class GPT(nn.Module):
118
+ def __init__(self, config):
119
+ super().__init__()
120
+ self.config = config
121
+ self.transformer = nn.ModuleDict(dict(
122
+ wte=nn.Embedding(config.vocab_size, config.n_embd),
123
+ h=nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
124
+ ln_f=nn.LayerNorm(config.n_embd),
125
+ ))
126
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
127
+ # tied embeddings
128
+ self.transformer.wte.weight = self.lm_head.weight
129
+
130
+ # RoPE cache (built lazily on first forward, cached on the module)
131
+ self.register_buffer("rope_cos", None, persistent=False)
132
+ self.register_buffer("rope_sin", None, persistent=False)
133
+
134
+ self.apply(self._init_weights)
135
+
136
+ def _init_weights(self, module):
137
+ if isinstance(module, nn.Linear):
138
+ std = 0.02
139
+ if hasattr(module, 'NANOGPT_SCALE_INIT'):
140
+ std *= (2 * self.config.n_layer) ** -0.5
141
+ torch.nn.init.normal_(module.weight, mean=0.0, std=std)
142
+ if module.bias is not None:
143
+ torch.nn.init.zeros_(module.bias)
144
+ elif isinstance(module, nn.Embedding):
145
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
146
+
147
+ def _ensure_rope(self, T, device):
148
+ head_dim = self.config.n_embd // self.config.n_head
149
+ if self.rope_cos is None or self.rope_cos.size(0) < T or self.rope_cos.device != device:
150
+ cos, sin = build_rope_cache(max(T, self.config.block_size), head_dim, device)
151
+ self.rope_cos, self.rope_sin = cos, sin
152
+
153
+ def forward(self, idx, targets=None):
154
+ B, T = idx.size()
155
+ assert T <= self.config.block_size, f"sequence length {T} > block_size {self.config.block_size}"
156
+ self._ensure_rope(T, idx.device)
157
+ cos, sin = self.rope_cos.to(idx.device), self.rope_sin.to(idx.device)
158
+
159
+ x = self.transformer.wte(idx) # (B, T, n_embd) β€” no positional embedding added; RoPE handles it
160
+ for block in self.transformer.h:
161
+ x = block(x, cos, sin)
162
+ x = self.transformer.ln_f(x)
163
+ logits = self.lm_head(x)
164
+ loss = None
165
+ if targets is not None:
166
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
167
+ return logits, loss
168
+
169
+ def num_params(self):
170
+ n = sum(p.numel() for p in self.parameters())
171
+ # subtract tied lm_head (shares wte weight) to avoid double counting
172
+ n -= self.lm_head.weight.numel()
173
+ return n
174
+
175
+ # -----------------------------------------------------------------------------
176
+ # quick self-test: prints param count so you can tune the config to ~250M
177
+ if __name__ == "__main__":
178
+ cfg = GPTConfig()
179
+ model = GPT(cfg)
180
+ print(f"config: n_layer={cfg.n_layer}, n_embd={cfg.n_embd}, "
181
+ f"n_head={cfg.n_head}, n_kv_head={cfg.n_kv_head}, vocab={cfg.vocab_size}")
182
+ print(f"total parameters: {model.num_params()/1e6:.1f}M")
183
+ # tiny forward sanity check on CPU
184
+ x = torch.randint(0, cfg.vocab_size, (2, 128))
185
+ logits, _ = model(x)
186
+ print(f"forward OK β€” logits shape {tuple(logits.shape)}")
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch
2
+ tokenizers
3
+ huggingface_hub
4
+ gradio>=4.44.0
5
+ numpy