dustylm-sdk 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: dustylm-sdk
3
+ Version: 0.1.1
4
+ Summary: The official Python SDK for DustyLM: an 8M-parameter model that talks like a robot vacuum.
5
+ Author-email: Mahmood Khordoo <m.khordoo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/khordoo/dustylm-sdk
8
+ Project-URL: Source, https://github.com/khordoo/dustylm-sdk
9
+ Keywords: llm,slm,small-language-model,transformer,pytorch,onnx,inference
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: torch>=2.0
20
+ Requires-Dist: tokenizers>=0.15
21
+ Requires-Dist: huggingface-hub>=0.20
22
+ Requires-Dist: numpy>=1.24
23
+ Provides-Extra: onnx
24
+ Requires-Dist: onnxruntime>=1.15; extra == "onnx"
25
+
26
+ <p align="center">
27
+ <img src="https://raw.githubusercontent.com/khordoo/dustylm-sdk/main/assets/logo.png" alt="DustyLM Logo" width="200"/>
28
+ </p>
29
+
30
+ # dustylm
31
+
32
+ Run DustyLM, the 8M parameter robot vacuum language model, in a few lines.
33
+
34
+ ```
35
+ pip install dustylm-sdk
36
+ ```
37
+
38
+ ```python
39
+ from dustylm import DustyLM
40
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft")
41
+ response = model.chat([{"role": "user", "content": "who are you?"}])
42
+ print(response["choices"][0]["message"]["content"])
43
+
44
+ # beep. i am a little robot. i clean floors and find crumbs.
45
+ ```
46
+
47
+ ## Backends
48
+
49
+ | Backend | Install | Model file | Best for |
50
+ |---|---|---:|---|
51
+ | `torch` (default) | `pip install dustylm-sdk` | ~32 MB (FP32) | Inspection and experimentation |
52
+ | `onnx` | `pip install dustylm-sdk[onnx]` | ~8–10 MB (int8) | A compact model artifact |
53
+
54
+ These sizes describe the model files, not the complete Python environment.
55
+ Installation size varies by platform. Both backends currently include PyTorch as
56
+ an SDK dependency.
57
+
58
+ ```python
59
+ # ONNX backend
60
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft", backend="onnx")
61
+ ```
62
+
63
+ ## Loading Local Checkpoints
64
+
65
+ Pass a Hugging Face repository ID to download its model and tokenizer
66
+ automatically, as shown in the quick-start example:
67
+
68
+ ```python
69
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft")
70
+ ```
71
+
72
+ For files already downloaded to your machine, pass their local directory
73
+ instead. With the default filenames, that directory must contain
74
+ `tokenizer.json` and either `model.pt` (PyTorch) or `model_int8.onnx` (ONNX):
75
+
76
+ ```python
77
+ model = DustyLM.from_pretrained("./my-checkpoint/")
78
+ ```
79
+
80
+ You can use any local directory as the artifact root. `model_file` and
81
+ `tokenizer_file` are resolved relative to that directory, so they may use
82
+ different filenames or relative paths.
83
+
84
+ If you trained a model with the main
85
+ [`dusty-lm`](https://github.com/khordoo/dusty-lm) repository, its checkpoint and
86
+ tokenizer are stored in separate artifact directories. Load the promoted SFT
87
+ checkpoint with:
88
+
89
+ ```python
90
+ from dustylm import DustyLM
91
+
92
+ model = DustyLM.from_pretrained(
93
+ "artifacts/checkpoints",
94
+ model_file="dusty8m_sft.pt",
95
+ tokenizer_file="../tokenizers/dusty_tokenizer.json",
96
+ )
97
+
98
+ response = model.chat([{"role": "user", "content": "who are you?"}])
99
+ ```
100
+
101
+ To inspect an intermediate checkpoint instead, change `model_file` to an
102
+ existing step file such as `dusty8m_sft_step_100.pt`.
103
+
104
+ The SDK infers several architecture dimensions from PyTorch checkpoint shapes
105
+ and currently expects DustyLM-compatible configurations.
106
+
107
+ ## API
108
+
109
+ ### DustyLM.from_pretrained
110
+
111
+ ```python
112
+ DustyLM.from_pretrained(
113
+ repo_id_or_path: str = "mkhordoo/dusty-8m-sft",
114
+ *,
115
+ model_file: str | None = None,
116
+ tokenizer_file: str | None = None,
117
+ backend: str = "torch",
118
+ )
119
+ ```
120
+
121
+ | Argument | Default | Description |
122
+ |---|---|---|
123
+ | `repo_id_or_path` | `"mkhordoo/dusty-8m-sft"` | HF Hub repo ID or local directory path |
124
+ | `model_file` | `"model.pt"` (torch) / `"model_int8.onnx"` (onnx) | Override the model filename in the directory |
125
+ | `tokenizer_file` | `"tokenizer.json"` | Override the tokenizer filename |
126
+ | `backend` | `"torch"` | `"torch"` or `"onnx"` |
127
+
128
+ ### DustyLM.chat
129
+
130
+ ```python
131
+ model.chat(
132
+ messages: list[dict],
133
+ temperature: float = 0.7,
134
+ max_tokens: int = 64,
135
+ top_p: float = 0.9,
136
+ ) -> dict
137
+ ```
138
+
139
+ Returns an OpenAI-style chat completion dict.
140
+
141
+ ---
142
+
143
+ Built from the [dusty-lm](https://github.com/khordoo/dusty-lm) training repository.
@@ -0,0 +1,118 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/khordoo/dustylm-sdk/main/assets/logo.png" alt="DustyLM Logo" width="200"/>
3
+ </p>
4
+
5
+ # dustylm
6
+
7
+ Run DustyLM, the 8M parameter robot vacuum language model, in a few lines.
8
+
9
+ ```
10
+ pip install dustylm-sdk
11
+ ```
12
+
13
+ ```python
14
+ from dustylm import DustyLM
15
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft")
16
+ response = model.chat([{"role": "user", "content": "who are you?"}])
17
+ print(response["choices"][0]["message"]["content"])
18
+
19
+ # beep. i am a little robot. i clean floors and find crumbs.
20
+ ```
21
+
22
+ ## Backends
23
+
24
+ | Backend | Install | Model file | Best for |
25
+ |---|---|---:|---|
26
+ | `torch` (default) | `pip install dustylm-sdk` | ~32 MB (FP32) | Inspection and experimentation |
27
+ | `onnx` | `pip install dustylm-sdk[onnx]` | ~8–10 MB (int8) | A compact model artifact |
28
+
29
+ These sizes describe the model files, not the complete Python environment.
30
+ Installation size varies by platform. Both backends currently include PyTorch as
31
+ an SDK dependency.
32
+
33
+ ```python
34
+ # ONNX backend
35
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft", backend="onnx")
36
+ ```
37
+
38
+ ## Loading Local Checkpoints
39
+
40
+ Pass a Hugging Face repository ID to download its model and tokenizer
41
+ automatically, as shown in the quick-start example:
42
+
43
+ ```python
44
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft")
45
+ ```
46
+
47
+ For files already downloaded to your machine, pass their local directory
48
+ instead. With the default filenames, that directory must contain
49
+ `tokenizer.json` and either `model.pt` (PyTorch) or `model_int8.onnx` (ONNX):
50
+
51
+ ```python
52
+ model = DustyLM.from_pretrained("./my-checkpoint/")
53
+ ```
54
+
55
+ You can use any local directory as the artifact root. `model_file` and
56
+ `tokenizer_file` are resolved relative to that directory, so they may use
57
+ different filenames or relative paths.
58
+
59
+ If you trained a model with the main
60
+ [`dusty-lm`](https://github.com/khordoo/dusty-lm) repository, its checkpoint and
61
+ tokenizer are stored in separate artifact directories. Load the promoted SFT
62
+ checkpoint with:
63
+
64
+ ```python
65
+ from dustylm import DustyLM
66
+
67
+ model = DustyLM.from_pretrained(
68
+ "artifacts/checkpoints",
69
+ model_file="dusty8m_sft.pt",
70
+ tokenizer_file="../tokenizers/dusty_tokenizer.json",
71
+ )
72
+
73
+ response = model.chat([{"role": "user", "content": "who are you?"}])
74
+ ```
75
+
76
+ To inspect an intermediate checkpoint instead, change `model_file` to an
77
+ existing step file such as `dusty8m_sft_step_100.pt`.
78
+
79
+ The SDK infers several architecture dimensions from PyTorch checkpoint shapes
80
+ and currently expects DustyLM-compatible configurations.
81
+
82
+ ## API
83
+
84
+ ### DustyLM.from_pretrained
85
+
86
+ ```python
87
+ DustyLM.from_pretrained(
88
+ repo_id_or_path: str = "mkhordoo/dusty-8m-sft",
89
+ *,
90
+ model_file: str | None = None,
91
+ tokenizer_file: str | None = None,
92
+ backend: str = "torch",
93
+ )
94
+ ```
95
+
96
+ | Argument | Default | Description |
97
+ |---|---|---|
98
+ | `repo_id_or_path` | `"mkhordoo/dusty-8m-sft"` | HF Hub repo ID or local directory path |
99
+ | `model_file` | `"model.pt"` (torch) / `"model_int8.onnx"` (onnx) | Override the model filename in the directory |
100
+ | `tokenizer_file` | `"tokenizer.json"` | Override the tokenizer filename |
101
+ | `backend` | `"torch"` | `"torch"` or `"onnx"` |
102
+
103
+ ### DustyLM.chat
104
+
105
+ ```python
106
+ model.chat(
107
+ messages: list[dict],
108
+ temperature: float = 0.7,
109
+ max_tokens: int = 64,
110
+ top_p: float = 0.9,
111
+ ) -> dict
112
+ ```
113
+
114
+ Returns an OpenAI-style chat completion dict.
115
+
116
+ ---
117
+
118
+ Built from the [dusty-lm](https://github.com/khordoo/dusty-lm) training repository.
@@ -0,0 +1,4 @@
1
+ from dustylm.inference import DustyLM
2
+
3
+ __all__ = ["DustyLM"]
4
+ __version__ = "0.1.1"
@@ -0,0 +1,461 @@
1
+ """Inference-only DustyLM — load from Hugging Face Hub or a local directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+
12
+ Backend = Literal["torch", "onnx"]
13
+
14
+ HUB_REPO = "mkhordoo/dusty-8m-sft"
15
+ HUB_FILES = {
16
+ "torch": "model.pt",
17
+ "onnx": "model_int8.onnx",
18
+ "tokenizer": "tokenizer.json",
19
+ }
20
+
21
+ CHATML_START_TOKEN = "<|im_start|>"
22
+ CHATML_END_TOKEN = "<|im_end|>"
23
+ SUPPORTED_ROLES = {"system", "user", "assistant"}
24
+
25
+ DEFAULT_CONFIG = {
26
+ "num_layers": 8,
27
+ "vocab_size": 4096,
28
+ "max_seq_len": 256,
29
+ "embed_dim": 256,
30
+ "num_heads": 8,
31
+ "num_kv_heads": 4,
32
+ "hidden_dim": 1024,
33
+ }
34
+
35
+
36
+ # ── Model (PyTorch) ──────────────────────────────────────────────────────────
37
+
38
+
39
+ def model_from_config(config: dict) -> "DustyLMModel":
40
+ model = DustyLMModel(**config)
41
+ return model
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ForwardContext:
46
+ position_ids: "torch.Tensor"
47
+ rope_sin: "torch.Tensor"
48
+ rope_cos: "torch.Tensor"
49
+
50
+
51
+ def _rotate_half(x):
52
+ x1 = x[..., : x.shape[-1] // 2]
53
+ x2 = x[..., x.shape[-1] // 2 :]
54
+ return torch.cat([-x2, x1], dim=-1)
55
+
56
+
57
+ def _apply_rope(x, sin, cos):
58
+ return x * cos + _rotate_half(x) * sin
59
+
60
+
61
+ class RotaryPositionalEmbedding(nn.Module):
62
+ def __init__(self, head_dim: int, max_seq_len: int, base: int = 10000):
63
+ super().__init__()
64
+ inv_freq = base ** (-torch.arange(0, head_dim, 2) / head_dim)
65
+ inv_freq = torch.cat([inv_freq, inv_freq], dim=-1)
66
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
67
+ self._build_cache(max_seq_len)
68
+
69
+ def _build_cache(self, max_seq_len: int):
70
+ positions = torch.arange(
71
+ max_seq_len, dtype=self.inv_freq.dtype, device=self.inv_freq.device
72
+ )
73
+ emb = torch.outer(positions, self.inv_freq)
74
+ self.register_buffer("sin_cache", emb.sin(), persistent=False)
75
+ self.register_buffer("cos_cache", emb.cos(), persistent=False)
76
+
77
+ def forward(self, position_ids):
78
+ sin = self.sin_cache[position_ids].unsqueeze(0).unsqueeze(1)
79
+ cos = self.cos_cache[position_ids].unsqueeze(0).unsqueeze(1)
80
+ return sin, cos
81
+
82
+
83
+ class MultiHeadAttention(nn.Module):
84
+ def __init__(self, embed_dim, num_heads, num_kv_heads, max_seq_len):
85
+ super().__init__()
86
+ self.num_heads = num_heads
87
+ self.num_kv_heads = num_kv_heads
88
+ self.head_dim = embed_dim // num_heads
89
+ self.kv_repeat = num_heads // num_kv_heads
90
+ self.qkv_proj = nn.Linear(
91
+ embed_dim, embed_dim + 2 * num_kv_heads * self.head_dim, bias=False
92
+ )
93
+ self.proj = nn.Linear(embed_dim, embed_dim, bias=False)
94
+
95
+ def forward(self, x, context, past_kv=None):
96
+ B, T, D = x.shape
97
+ qkv = self.qkv_proj(x)
98
+ q_dim = self.num_heads * self.head_dim
99
+ kv_dim = self.num_kv_heads * self.head_dim
100
+ q, k, v = qkv.split([q_dim, kv_dim, kv_dim], dim=-1)
101
+
102
+ q = q.reshape(B, T, self.num_heads, self.head_dim).transpose(1, 2)
103
+ k = k.reshape(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
104
+ v = v.reshape(B, T, self.num_kv_heads, self.head_dim).transpose(1, 2)
105
+
106
+ q = _apply_rope(q, context.rope_sin, context.rope_cos)
107
+ k = _apply_rope(k, context.rope_sin, context.rope_cos)
108
+
109
+ past_len = 0
110
+ if past_kv is not None:
111
+ pk, pv = past_kv
112
+ past_len = pk.shape[2]
113
+ k = torch.cat([pk, k], dim=2)
114
+ v = torch.cat([pv, v], dim=2)
115
+
116
+ present_kv = (k, v)
117
+ k = k.repeat_interleave(self.kv_repeat, dim=1)
118
+ v = v.repeat_interleave(self.kv_repeat, dim=1)
119
+
120
+ scores = q @ k.transpose(-2, -1) / (self.head_dim**0.5)
121
+ if T > 1:
122
+ total_len = past_len + T
123
+ mask = torch.triu(
124
+ torch.ones(T, total_len, device=x.device, dtype=torch.bool),
125
+ diagonal=past_len + 1,
126
+ )
127
+ scores = scores.masked_fill(mask, float("-inf"))
128
+
129
+ attn = torch.softmax(scores, dim=-1)
130
+ out = (attn @ v).transpose(1, 2).reshape(B, T, D)
131
+ return self.proj(out), present_kv
132
+
133
+
134
+ class TransformerBlock(nn.Module):
135
+ def __init__(
136
+ self, embed_dim, num_heads, num_kv_heads, max_seq_len, hidden_dim=None
137
+ ):
138
+ super().__init__()
139
+ hidden_dim = hidden_dim or 4 * embed_dim
140
+ self.att_norm = nn.RMSNorm(embed_dim)
141
+ self.attention = MultiHeadAttention(
142
+ embed_dim, num_heads, num_kv_heads, max_seq_len
143
+ )
144
+ self.mlp = nn.Sequential(
145
+ nn.Linear(embed_dim, hidden_dim, bias=False),
146
+ nn.GELU(),
147
+ nn.Linear(hidden_dim, embed_dim, bias=False),
148
+ )
149
+ self.mlp_norm = nn.RMSNorm(embed_dim)
150
+
151
+ def forward(self, x, context, kv_cache=None):
152
+ a, pkv = self.attention(self.att_norm(x), context, kv_cache)
153
+ x = x + a
154
+ x = x + self.mlp(self.mlp_norm(x))
155
+ return x, pkv
156
+
157
+
158
+ class DustyLMModel(nn.Module):
159
+ def __init__(
160
+ self,
161
+ num_layers,
162
+ vocab_size,
163
+ max_seq_len,
164
+ embed_dim,
165
+ num_heads,
166
+ num_kv_heads,
167
+ hidden_dim=None,
168
+ rope_base=10000,
169
+ ):
170
+ super().__init__()
171
+ head_dim = embed_dim // num_heads
172
+ self.rope = RotaryPositionalEmbedding(head_dim, max_seq_len, rope_base)
173
+ self.embed = nn.Embedding(vocab_size, embed_dim)
174
+ self.layers = nn.ModuleList(
175
+ [
176
+ TransformerBlock(
177
+ embed_dim, num_heads, num_kv_heads, max_seq_len, hidden_dim
178
+ )
179
+ for _ in range(num_layers)
180
+ ]
181
+ )
182
+ self.final_norm = nn.RMSNorm(embed_dim)
183
+ self.vocab_proj = nn.Linear(embed_dim, vocab_size, bias=False)
184
+ self.max_seq_len = max_seq_len
185
+
186
+ def forward(self, x, kv_cache=None):
187
+ B, T = x.shape
188
+ use_cache = kv_cache is not None
189
+
190
+ past_len = 0
191
+ if use_cache and kv_cache[0] is not None:
192
+ past_len = kv_cache[0][0].shape[2]
193
+
194
+ position_ids = torch.arange(
195
+ past_len, past_len + T, dtype=torch.long, device=x.device
196
+ )
197
+ sin, cos = self.rope(position_ids)
198
+ ctx = ForwardContext(position_ids=position_ids, rope_sin=sin, rope_cos=cos)
199
+
200
+ h = self.embed(x)
201
+ next_kv = [] if use_cache else None
202
+ for i, layer in enumerate(self.layers):
203
+ h, pkv = layer(h, ctx, kv_cache[i] if use_cache else None)
204
+ if use_cache:
205
+ next_kv.append(pkv)
206
+
207
+ logits = self.vocab_proj(self.final_norm(h))
208
+ if use_cache:
209
+ return logits, next_kv
210
+ return logits
211
+
212
+
213
+ # ── Tokenizer ─────────────────────────────────────────────────────────────────
214
+
215
+
216
+ def load_tokenizer(path: Path):
217
+ from tokenizers import Tokenizer
218
+
219
+ return Tokenizer.from_file(str(path))
220
+
221
+
222
+ def format_chatml(messages: list[dict]) -> str:
223
+ chunks = []
224
+ for msg in messages:
225
+ role, content = msg.get("role", "user"), msg.get("content", "")
226
+ if not content.strip():
227
+ continue
228
+ chunks.append(f"{CHATML_START_TOKEN}{role}\n{content}{CHATML_END_TOKEN}\n")
229
+ chunks.append(f"{CHATML_START_TOKEN}assistant\n")
230
+ return "".join(chunks)
231
+
232
+
233
+ # ── Sampling ──────────────────────────────────────────────────────────────────
234
+
235
+
236
+ def sample(logits, temperature=0.7, top_p=0.9):
237
+ logits = logits / max(temperature, 1e-8)
238
+ probs = torch.softmax(logits, dim=-1)
239
+
240
+ if top_p < 1.0:
241
+ sorted_probs, sorted_indices = torch.sort(probs, descending=True)
242
+ cumsum = torch.cumsum(sorted_probs, dim=-1)
243
+ remove = cumsum > top_p
244
+ remove[..., 1:] = remove[..., :-1].clone()
245
+ remove[..., 0] = False
246
+ sorted_probs[remove] = 0.0
247
+ probs = sorted_probs / sorted_probs.sum(dim=-1, keepdim=True)
248
+ probs = probs.gather(-1, sorted_indices.argsort(-1))
249
+
250
+ return int(torch.multinomial(probs, 1).item())
251
+
252
+
253
+ # ── Main class ───────────────────────────────────────────────────────────────
254
+
255
+
256
+ class DustyLM:
257
+ """Load and chat with a DustyLM model."""
258
+
259
+ def __init__(self, model, tokenizer, config: dict, backend: Backend):
260
+ self._model = model
261
+ self._tokenizer = tokenizer
262
+ self._config = config
263
+ self._backend = backend
264
+
265
+ @classmethod
266
+ def from_pretrained(
267
+ cls,
268
+ repo_id_or_path: str = HUB_REPO,
269
+ *,
270
+ model_file: str | None = None,
271
+ tokenizer_file: str | None = None,
272
+ backend: Backend = "torch",
273
+ ) -> "DustyLM":
274
+ """Load from Hugging Face Hub or a local directory.
275
+
276
+ Args:
277
+ repo_id_or_path: HF repo ID (e.g. "mkhordoo/dusty-8m-sft")
278
+ or local directory path.
279
+ model_file: Model filename in the directory (default depends on backend:
280
+ "model.pt" for torch, "model_int8.onnx" for onnx).
281
+ tokenizer_file: Tokenizer filename (default "tokenizer.json").
282
+ backend: "torch" (default) or "onnx".
283
+ """
284
+ model_file = model_file or HUB_FILES[backend if backend == "onnx" else "torch"]
285
+ tokenizer_file = tokenizer_file or HUB_FILES["tokenizer"]
286
+
287
+ path = Path(repo_id_or_path)
288
+ is_local = path.exists()
289
+
290
+ if is_local:
291
+ tokenizer_path = path / tokenizer_file
292
+ else:
293
+ from huggingface_hub import hf_hub_download
294
+
295
+ tokenizer_path = Path(
296
+ hf_hub_download(repo_id=repo_id_or_path, filename=tokenizer_file)
297
+ )
298
+
299
+ tokenizer = load_tokenizer(tokenizer_path)
300
+
301
+ if backend == "torch":
302
+ model, config = cls._load_torch(repo_id_or_path, model_file, is_local)
303
+ elif backend == "onnx":
304
+ model, config = cls._load_onnx(repo_id_or_path, model_file, is_local)
305
+ else:
306
+ raise ValueError(f"Unknown backend: {backend}")
307
+
308
+ return cls(model=model, tokenizer=tokenizer, config=config, backend=backend)
309
+
310
+ @classmethod
311
+ def _load_torch(cls, repo_id_or_path, model_file, is_local):
312
+ if is_local:
313
+ ckpt_path = Path(repo_id_or_path) / model_file
314
+ else:
315
+ from huggingface_hub import hf_hub_download
316
+
317
+ ckpt_path = Path(
318
+ hf_hub_download(repo_id=repo_id_or_path, filename=model_file)
319
+ )
320
+
321
+ state = torch.load(ckpt_path, map_location="cpu", weights_only=True)
322
+ state.pop("rope.sin_cache", None)
323
+ state.pop("rope.cos_cache", None)
324
+
325
+ config = dict(DEFAULT_CONFIG)
326
+ config["vocab_size"] = state["vocab_proj.weight"].shape[0]
327
+ config["embed_dim"] = state["embed.weight"].shape[1]
328
+ config["num_layers"] = sum(
329
+ 1
330
+ for k in state
331
+ if k.startswith("layers.") and k.endswith(".att_norm.weight")
332
+ )
333
+ qkv_out = state["layers.0.attention.qkv_proj.weight"].shape[0]
334
+ head_dim = config["embed_dim"] // config["num_heads"]
335
+ num_kv_heads = (qkv_out - config["embed_dim"]) // (2 * head_dim)
336
+ config["num_kv_heads"] = num_kv_heads
337
+ config["num_heads"] = config["num_heads"] or config["num_kv_heads"]
338
+ config["hidden_dim"] = state["layers.0.mlp.0.weight"].shape[0]
339
+
340
+ model = model_from_config(config)
341
+ model.load_state_dict(state)
342
+ model.eval()
343
+ return model, config
344
+
345
+ @classmethod
346
+ def _load_onnx(cls, repo_id_or_path, model_file, is_local):
347
+ import onnxruntime as ort
348
+
349
+ if is_local:
350
+ onnx_path = str(Path(repo_id_or_path) / model_file)
351
+ else:
352
+ from huggingface_hub import hf_hub_download
353
+
354
+ onnx_path = hf_hub_download(repo_id=repo_id_or_path, filename=model_file)
355
+
356
+ session = ort.InferenceSession(onnx_path)
357
+ config = dict(DEFAULT_CONFIG)
358
+ return session, config
359
+
360
+ def chat(
361
+ self,
362
+ messages: list[dict[str, Any]],
363
+ temperature: float = 0.7,
364
+ max_tokens: int = 64,
365
+ top_p: float = 0.9,
366
+ ) -> dict:
367
+ """Chat completion returning an OpenAI-style response dict."""
368
+ prompt = format_chatml(messages)
369
+
370
+ if self._backend == "torch":
371
+ return self._chat_torch(prompt, temperature, max_tokens, top_p)
372
+ else:
373
+ return self._chat_onnx(prompt, temperature, max_tokens, top_p)
374
+
375
+ def _chat_torch(self, prompt, temperature, max_tokens, top_p):
376
+ prompt_ids = self._tokenizer.encode(prompt).ids
377
+ max_seq = self._config["max_seq_len"]
378
+
379
+ input_ids = torch.tensor([prompt_ids])
380
+ past_len = input_ids.shape[1]
381
+
382
+ if max_seq - past_len < max_tokens:
383
+ max_tokens = max_seq - past_len
384
+
385
+ if max_tokens <= 0:
386
+ return self._response("", 0)
387
+
388
+ model = self._model
389
+ device = next(model.parameters()).device
390
+ input_ids = input_ids.to(device)
391
+
392
+ num_layers = self._config["num_layers"]
393
+ kv_cache = [None] * num_layers
394
+ logits, kv_cache = model(input_ids, kv_cache)
395
+ end_token_id = self._tokenizer.token_to_id(CHATML_END_TOKEN)
396
+ generated = []
397
+
398
+ for _ in range(max_tokens):
399
+ next_id = sample(logits[0, -1, :], temperature, top_p)
400
+ generated.append(next_id)
401
+ if next_id == end_token_id:
402
+ break
403
+
404
+ inp = torch.tensor([[next_id]], device=device)
405
+ logits, kv_cache = model(inp, kv_cache)
406
+
407
+ content = self._decode_completion(generated)
408
+ return self._response(content, len(generated))
409
+
410
+ def _chat_onnx(self, prompt, temperature, max_tokens, top_p):
411
+ import numpy as np
412
+
413
+ prompt_ids = self._tokenizer.encode(prompt).ids
414
+ max_seq = self._config["max_seq_len"]
415
+
416
+ gen_count = min(max_tokens, max_seq - len(prompt_ids))
417
+ if gen_count <= 0:
418
+ content = self._tokenizer.decode(prompt_ids).strip()
419
+ return self._response(content, 0)
420
+
421
+ session = self._model
422
+ end_token_id = self._tokenizer.token_to_id(CHATML_END_TOKEN)
423
+ generated = []
424
+
425
+ for step in range(gen_count):
426
+ input_ids = np.array([prompt_ids + generated], dtype=np.int64)
427
+ outputs = session.run(None, {"input_ids": input_ids})
428
+ logits = outputs[0]
429
+ last_logits = torch.tensor(logits[0, -1, :])
430
+ next_id = sample(last_logits, temperature, top_p)
431
+ generated.append(next_id)
432
+ if next_id == end_token_id:
433
+ break
434
+
435
+ content = self._decode_completion(generated)
436
+ return self._response(content, len(generated))
437
+
438
+ def _decode_completion(self, token_ids: list[int]) -> str:
439
+ end_token_id = self._tokenizer.token_to_id(CHATML_END_TOKEN)
440
+ if end_token_id in token_ids:
441
+ token_ids = token_ids[: token_ids.index(end_token_id)]
442
+ return self._tokenizer.decode(token_ids).strip()
443
+
444
+ @staticmethod
445
+ def _response(content: str, completion_tokens: int) -> dict:
446
+ return {
447
+ "object": "chat.completion",
448
+ "model": "dusty8m",
449
+ "choices": [
450
+ {
451
+ "index": 0,
452
+ "message": {"role": "assistant", "content": content},
453
+ "finish_reason": "stop" if completion_tokens > 0 else "length",
454
+ }
455
+ ],
456
+ "usage": {
457
+ "prompt_tokens": 0,
458
+ "completion_tokens": completion_tokens,
459
+ "total_tokens": completion_tokens,
460
+ },
461
+ }
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: dustylm-sdk
3
+ Version: 0.1.1
4
+ Summary: The official Python SDK for DustyLM: an 8M-parameter model that talks like a robot vacuum.
5
+ Author-email: Mahmood Khordoo <m.khordoo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/khordoo/dustylm-sdk
8
+ Project-URL: Source, https://github.com/khordoo/dustylm-sdk
9
+ Keywords: llm,slm,small-language-model,transformer,pytorch,onnx,inference
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: torch>=2.0
20
+ Requires-Dist: tokenizers>=0.15
21
+ Requires-Dist: huggingface-hub>=0.20
22
+ Requires-Dist: numpy>=1.24
23
+ Provides-Extra: onnx
24
+ Requires-Dist: onnxruntime>=1.15; extra == "onnx"
25
+
26
+ <p align="center">
27
+ <img src="https://raw.githubusercontent.com/khordoo/dustylm-sdk/main/assets/logo.png" alt="DustyLM Logo" width="200"/>
28
+ </p>
29
+
30
+ # dustylm
31
+
32
+ Run DustyLM, the 8M parameter robot vacuum language model, in a few lines.
33
+
34
+ ```
35
+ pip install dustylm-sdk
36
+ ```
37
+
38
+ ```python
39
+ from dustylm import DustyLM
40
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft")
41
+ response = model.chat([{"role": "user", "content": "who are you?"}])
42
+ print(response["choices"][0]["message"]["content"])
43
+
44
+ # beep. i am a little robot. i clean floors and find crumbs.
45
+ ```
46
+
47
+ ## Backends
48
+
49
+ | Backend | Install | Model file | Best for |
50
+ |---|---|---:|---|
51
+ | `torch` (default) | `pip install dustylm-sdk` | ~32 MB (FP32) | Inspection and experimentation |
52
+ | `onnx` | `pip install dustylm-sdk[onnx]` | ~8–10 MB (int8) | A compact model artifact |
53
+
54
+ These sizes describe the model files, not the complete Python environment.
55
+ Installation size varies by platform. Both backends currently include PyTorch as
56
+ an SDK dependency.
57
+
58
+ ```python
59
+ # ONNX backend
60
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft", backend="onnx")
61
+ ```
62
+
63
+ ## Loading Local Checkpoints
64
+
65
+ Pass a Hugging Face repository ID to download its model and tokenizer
66
+ automatically, as shown in the quick-start example:
67
+
68
+ ```python
69
+ model = DustyLM.from_pretrained("mkhordoo/dusty-8m-sft")
70
+ ```
71
+
72
+ For files already downloaded to your machine, pass their local directory
73
+ instead. With the default filenames, that directory must contain
74
+ `tokenizer.json` and either `model.pt` (PyTorch) or `model_int8.onnx` (ONNX):
75
+
76
+ ```python
77
+ model = DustyLM.from_pretrained("./my-checkpoint/")
78
+ ```
79
+
80
+ You can use any local directory as the artifact root. `model_file` and
81
+ `tokenizer_file` are resolved relative to that directory, so they may use
82
+ different filenames or relative paths.
83
+
84
+ If you trained a model with the main
85
+ [`dusty-lm`](https://github.com/khordoo/dusty-lm) repository, its checkpoint and
86
+ tokenizer are stored in separate artifact directories. Load the promoted SFT
87
+ checkpoint with:
88
+
89
+ ```python
90
+ from dustylm import DustyLM
91
+
92
+ model = DustyLM.from_pretrained(
93
+ "artifacts/checkpoints",
94
+ model_file="dusty8m_sft.pt",
95
+ tokenizer_file="../tokenizers/dusty_tokenizer.json",
96
+ )
97
+
98
+ response = model.chat([{"role": "user", "content": "who are you?"}])
99
+ ```
100
+
101
+ To inspect an intermediate checkpoint instead, change `model_file` to an
102
+ existing step file such as `dusty8m_sft_step_100.pt`.
103
+
104
+ The SDK infers several architecture dimensions from PyTorch checkpoint shapes
105
+ and currently expects DustyLM-compatible configurations.
106
+
107
+ ## API
108
+
109
+ ### DustyLM.from_pretrained
110
+
111
+ ```python
112
+ DustyLM.from_pretrained(
113
+ repo_id_or_path: str = "mkhordoo/dusty-8m-sft",
114
+ *,
115
+ model_file: str | None = None,
116
+ tokenizer_file: str | None = None,
117
+ backend: str = "torch",
118
+ )
119
+ ```
120
+
121
+ | Argument | Default | Description |
122
+ |---|---|---|
123
+ | `repo_id_or_path` | `"mkhordoo/dusty-8m-sft"` | HF Hub repo ID or local directory path |
124
+ | `model_file` | `"model.pt"` (torch) / `"model_int8.onnx"` (onnx) | Override the model filename in the directory |
125
+ | `tokenizer_file` | `"tokenizer.json"` | Override the tokenizer filename |
126
+ | `backend` | `"torch"` | `"torch"` or `"onnx"` |
127
+
128
+ ### DustyLM.chat
129
+
130
+ ```python
131
+ model.chat(
132
+ messages: list[dict],
133
+ temperature: float = 0.7,
134
+ max_tokens: int = 64,
135
+ top_p: float = 0.9,
136
+ ) -> dict
137
+ ```
138
+
139
+ Returns an OpenAI-style chat completion dict.
140
+
141
+ ---
142
+
143
+ Built from the [dusty-lm](https://github.com/khordoo/dusty-lm) training repository.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ dustylm/__init__.py
4
+ dustylm/inference.py
5
+ dustylm_sdk.egg-info/PKG-INFO
6
+ dustylm_sdk.egg-info/SOURCES.txt
7
+ dustylm_sdk.egg-info/dependency_links.txt
8
+ dustylm_sdk.egg-info/requires.txt
9
+ dustylm_sdk.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ torch>=2.0
2
+ tokenizers>=0.15
3
+ huggingface-hub>=0.20
4
+ numpy>=1.24
5
+
6
+ [onnx]
7
+ onnxruntime>=1.15
@@ -0,0 +1 @@
1
+ dustylm
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dustylm-sdk"
7
+ version = "0.1.1"
8
+ description = "The official Python SDK for DustyLM: an 8M-parameter model that talks like a robot vacuum."
9
+ keywords = [
10
+ "llm",
11
+ "slm",
12
+ "small-language-model",
13
+ "transformer",
14
+ "pytorch",
15
+ "onnx",
16
+ "inference"
17
+ ]
18
+ readme = "README.md"
19
+ license = "MIT"
20
+ authors = [
21
+ { name = "Mahmood Khordoo", email = "m.khordoo@gmail.com" },
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 3 - Alpha",
25
+ "Intended Audience :: Developers",
26
+ "Intended Audience :: Education",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
31
+ ]
32
+ requires-python = ">=3.11"
33
+ dependencies = [
34
+ "torch>=2.0",
35
+ "tokenizers>=0.15",
36
+ "huggingface-hub>=0.20",
37
+ "numpy>=1.24",
38
+ ]
39
+
40
+ [project.optional-dependencies]
41
+ onnx = [
42
+ "onnxruntime>=1.15",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/khordoo/dustylm-sdk"
47
+ Source = "https://github.com/khordoo/dustylm-sdk"
48
+
49
+ [tool.setuptools.packages.find]
50
+ include = ["dustylm*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+