vigneshwar234 commited on
Commit
ebaad48
·
verified ·
1 Parent(s): e5794d6

Larger corpus: full-text papers, smoltalk2, resumable training

Browse files
Files changed (4) hide show
  1. README.md +25 -15
  2. Vashtra_training.ipynb +40 -18
  3. build_corpus.py +128 -69
  4. train_vashtra.py +72 -14
README.md CHANGED
@@ -18,9 +18,10 @@ tags:
18
  - webgpu
19
  datasets:
20
  - vigneshwar234/vashtra-ml-corpus
 
21
  - CShorten/ML-ArXiv-Papers
22
  - donfu/oa-stackexchange
23
- - HuggingFaceTB/smol-smoltalk
24
  ---
25
 
26
  # Vashtra-0.6B
@@ -43,19 +44,29 @@ does.
43
  Two stages, both full-parameter. At 0.6B a full fine-tune beats LoRA for domain
44
  adaptation and still fits in a free Colab T4, using about 10GB of 16GB.
45
 
46
- **Stage A, continued pretraining.** 117k arXiv `cs.LG` titles and abstracts, packed
47
- into dense 1024-token blocks so no compute goes to padding. LR 5e-5, cosine, one
48
- epoch. The LR is low on purpose. The goal is to absorb the domain without wrecking
49
- the base model's general ability.
 
 
 
 
 
 
 
 
 
50
 
51
  **Stage B, supervised fine-tuning.** ML, stats and CS StackExchange answers that were
52
- accepted with score 3 or higher, plus arXiv explanation and titling tasks, plus a
53
- general chat anchor. LR 1e-5, two epochs, loss masked so only assistant turns count.
 
54
 
55
  ### The general chat anchor
56
 
57
  Fine-tuning a 0.6B model only on domain data reliably destroys its ability to hold a
58
- normal conversation. About 20k general examples from `smol-smoltalk` stay in the SFT
59
  mixture to stop that. It costs a little domain sharpness and keeps the model usable.
60
 
61
  ### A tokenisation detail
@@ -104,16 +115,15 @@ Useful for explaining ML concepts, summarising papers, and on-device or edge cas
104
  where 0.6B is the size you can afford.
105
 
106
  It is still a 0.6B model. It gets specifics wrong, especially exact numbers, named
107
- papers, and anything recent. The arXiv data is abstracts only, so it knows what
108
- papers claim, not what they showed. Do not cite it and do not use it for anything
109
- that matters without checking the answer.
110
 
111
  ## Licence
112
 
113
- Apache-2.0, same as the Qwen3-0.6B base. The corpus mixes licences: arXiv abstracts
114
- are AFL-3.0 through `CShorten/ML-ArXiv-Papers`, StackExchange content is CC-BY-SA-4.0,
115
- and `smol-smoltalk` is Apache-2.0. Anything derived from StackExchange stays
116
- CC-BY-SA-4.0.
117
 
118
  Built by [vigneshwar234](https://huggingface.co/vigneshwar234).
119
  Code on [GitHub](https://github.com/vignesh2027/Vashtra).
 
18
  - webgpu
19
  datasets:
20
  - vigneshwar234/vashtra-ml-corpus
21
+ - common-pile/arxiv_papers_filtered
22
  - CShorten/ML-ArXiv-Papers
23
  - donfu/oa-stackexchange
24
+ - HuggingFaceTB/smoltalk2
25
  ---
26
 
27
  # Vashtra-0.6B
 
44
  Two stages, both full-parameter. At 0.6B a full fine-tune beats LoRA for domain
45
  adaptation and still fits in a free Colab T4, using about 10GB of 16GB.
46
 
47
+ **Stage A, continued pretraining.** Full-text arXiv ML papers from `common-pile`
48
+ (openly licensed only) plus 117k arXiv `cs.LG` titles and abstracts, packed into
49
+ dense 1024-token blocks so no compute goes to padding. LR 5e-5, cosine, one epoch.
50
+ The LR is low on purpose. The goal is to absorb the domain without wrecking the
51
+ base model's general ability.
52
+
53
+ Full text matters here. Abstracts teach the model what papers claim. Full text
54
+ teaches it how the argument is built, which is what you want when someone asks it
55
+ to explain a method.
56
+
57
+ The corpus is about 90M tokens, more than a free Colab session will finish, so
58
+ stage A takes a `--cpt_tokens` budget and pushes checkpoints to the Hub. Passing
59
+ `--resume` picks up where a dropped session stopped.
60
 
61
  **Stage B, supervised fine-tuning.** ML, stats and CS StackExchange answers that were
62
+ accepted with score 3 or higher, arXiv explanation and titling tasks, and three
63
+ subsets of `smoltalk2`: science reasoning, instruction following, and general chat.
64
+ LR 1e-5, two epochs, loss masked so only assistant turns count.
65
 
66
  ### The general chat anchor
67
 
68
  Fine-tuning a 0.6B model only on domain data reliably destroys its ability to hold a
69
+ normal conversation. About 22k general examples from `smoltalk2` stay in the SFT
70
  mixture to stop that. It costs a little domain sharpness and keeps the model usable.
71
 
72
  ### A tokenisation detail
 
115
  where 0.6B is the size you can afford.
116
 
117
  It is still a 0.6B model. It gets specifics wrong, especially exact numbers, named
118
+ papers, and anything recent. Do not cite it and do not use it for anything that
119
+ matters without checking the answer.
 
120
 
121
  ## Licence
122
 
123
+ Apache-2.0, same as the Qwen3-0.6B base. The corpus mixes licences: full-text papers
124
+ are CC-BY, CC-BY-SA or CC0 through `common-pile/arxiv_papers_filtered`, abstracts are
125
+ AFL-3.0 through `CShorten/ML-ArXiv-Papers`, StackExchange content is CC-BY-SA-4.0,
126
+ and `smoltalk2` is Apache-2.0. Anything derived from StackExchange stays CC-BY-SA-4.0.
127
 
128
  Built by [vigneshwar234](https://huggingface.co/vigneshwar234).
129
  Code on [GitHub](https://github.com/vignesh2027/Vashtra).
Vashtra_training.ipynb CHANGED
@@ -4,17 +4,17 @@
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
- "# Vashtra-0.6B \u2014 build the corpus, train the model, ship the demo\n",
8
  "\n",
9
  "Two-stage domain adaptation of **Qwen3-0.6B** into a machine-learning specialist.\n",
10
  "\n",
11
  "| Stage | Data | What it teaches |\n",
12
  "|---|---|---|\n",
13
- "| A \u2014 continued pretraining | 117k arXiv cs.LG abstracts | ML vocabulary and concepts |\n",
14
- "| B \u2014 supervised fine-tuning | ML Q&A + general chat | answering, instruction following |\n",
15
  "\n",
16
  "**Runtime \u2192 Change runtime type \u2192 T4 GPU**, then **Runtime \u2192 Run all**.\n",
17
- "Everything pushes to the Hub automatically. Budget ~2\u20133 hours on the free tier.\n"
18
  ]
19
  },
20
  {
@@ -22,7 +22,7 @@
22
  "metadata": {},
23
  "source": [
24
  "### 1. Confirm you have a GPU\n",
25
- "If this cell fails you are on CPU \u2014 switch the runtime first, or nothing below will finish."
26
  ]
27
  },
28
  {
@@ -102,12 +102,12 @@
102
  "source": [
103
  "### 5. Build the corpus\n",
104
  "\n",
105
- "Streams arXiv cs.LG, ML/stats/CS StackExchange, and the general-chat anchor into\n",
106
- "`cpt.jsonl` + `sft.jsonl`, then pushes them to\n",
107
- "`vigneshwar234/vashtra-ml-corpus`. Takes ~15\u201325 minutes, mostly the\n",
108
- "StackExchange scan.\n",
109
  "\n",
110
- "Skip this cell if the corpus is already built \u2014 training pulls it from the Hub."
111
  ]
112
  },
113
  {
@@ -128,8 +128,30 @@
128
  "Stage A (continued pretraining) then Stage B (SFT), then the model is pushed to\n",
129
  "`vigneshwar234/Vashtra-0.6B`.\n",
130
  "\n",
131
- "Running short on session time? Use `--cpt_docs 40000 --sft_epochs 1` to cut\n",
132
- "stage A and B down substantially."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  ]
134
  },
135
  {
@@ -138,7 +160,7 @@
138
  "execution_count": null,
139
  "outputs": [],
140
  "source": [
141
- "!python train_vashtra.py --stage all"
142
  ]
143
  },
144
  {
@@ -147,7 +169,7 @@
147
  "source": [
148
  "### 7. Smoke-test what you trained\n",
149
  "\n",
150
- "Sanity check, not an evaluation. You are looking for coherent, on-topic ML answers \u2014\n",
151
  "and for the general question to still work, which is what the anchor data buys."
152
  ]
153
  },
@@ -183,8 +205,8 @@
183
  "### 8. Measure it against the base model\n",
184
  "\n",
185
  "Compare perplexity on held-out arXiv ML text. If stage A worked, Vashtra should be\n",
186
- "meaningfully lower than Qwen3-0.6B here. Put the real number in the model card \u2014\n",
187
- "this is the claim that makes the model credible."
188
  ]
189
  },
190
  {
@@ -228,7 +250,7 @@
228
  "### 9. Export to ONNX for the in-browser demo\n",
229
  "\n",
230
  "Produces `vigneshwar234/Vashtra-0.6B-ONNX`, which the static WebGPU Space loads\n",
231
- "directly \u2014 so the demo costs nothing to host and runs on the visitor's own GPU."
232
  ]
233
  },
234
  {
@@ -257,7 +279,7 @@
257
  "\n",
258
  "**Last step:** put the real perplexity number from cell 8 into the model card, and\n",
259
  "replace the \"weights are training\" banner. Measured results are what make people\n",
260
- "trust a small model \u2014 ship them.\n"
261
  ]
262
  }
263
  ],
 
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
+ "# Vashtra-0.6B: build the corpus, train the model, ship the demo\n",
8
  "\n",
9
  "Two-stage domain adaptation of **Qwen3-0.6B** into a machine-learning specialist.\n",
10
  "\n",
11
  "| Stage | Data | What it teaches |\n",
12
  "|---|---|---|\n",
13
+ "| A, continued pretraining | full-text arXiv ML papers + 117k cs.LG abstracts | ML vocabulary, concepts, how arguments are made |\n",
14
+ "| B, supervised fine-tuning | ML Q&A + general chat | answering, instruction following |\n",
15
  "\n",
16
  "**Runtime \u2192 Change runtime type \u2192 T4 GPU**, then **Runtime \u2192 Run all**.\n",
17
+ "Everything pushes to the Hub automatically. Budget ~2 to 3 hours on the free tier.\n"
18
  ]
19
  },
20
  {
 
22
  "metadata": {},
23
  "source": [
24
  "### 1. Confirm you have a GPU\n",
25
+ "If this cell fails you are on CPU. Switch the runtime first, or nothing below will finish."
26
  ]
27
  },
28
  {
 
102
  "source": [
103
  "### 5. Build the corpus\n",
104
  "\n",
105
+ "Streams full-text arXiv papers, cs.LG abstracts, ML/stats/CS StackExchange, and\n",
106
+ "the smoltalk2 mixture into `cpt.jsonl` + `sft.jsonl`, then pushes them to\n",
107
+ "`vigneshwar234/vashtra-ml-corpus`. Takes ~25 to 40 minutes, mostly the\n",
108
+ "StackExchange and full-text scans.\n",
109
  "\n",
110
+ "Skip this cell if the corpus is already built. Training pulls it from the Hub."
111
  ]
112
  },
113
  {
 
128
  "Stage A (continued pretraining) then Stage B (SFT), then the model is pushed to\n",
129
  "`vigneshwar234/Vashtra-0.6B`.\n",
130
  "\n",
131
+ "The corpus is about 90M tokens, which is more than a free Colab session will get\n",
132
+ "through. Stage A therefore takes a token budget, defaulting to 30M, which is\n",
133
+ "roughly two hours on a T4. Checkpoints are pushed to the Hub as it goes.\n",
134
+ "\n",
135
+ "If the session drops, open a new one and run the same cell with `--resume`. It\n",
136
+ "pulls the last checkpoint back down and carries on instead of starting over.\n",
137
+ "To train on more of the corpus, raise `--cpt_tokens` (or pass `0` for all of it)\n",
138
+ "and expect to resume across two or three sessions."
139
+ ]
140
+ },
141
+ {
142
+ "cell_type": "code",
143
+ "metadata": {},
144
+ "execution_count": null,
145
+ "outputs": [],
146
+ "source": [
147
+ "!python train_vashtra.py --stage all --cpt_tokens 30000000"
148
+ ]
149
+ },
150
+ {
151
+ "cell_type": "markdown",
152
+ "metadata": {},
153
+ "source": [
154
+ "If the session above was cut off, run this instead of starting over."
155
  ]
156
  },
157
  {
 
160
  "execution_count": null,
161
  "outputs": [],
162
  "source": [
163
+ "# !python train_vashtra.py --stage all --cpt_tokens 30000000 --resume"
164
  ]
165
  },
166
  {
 
169
  "source": [
170
  "### 7. Smoke-test what you trained\n",
171
  "\n",
172
+ "Sanity check, not an evaluation. You are looking for coherent, on-topic ML answers,\n",
173
  "and for the general question to still work, which is what the anchor data buys."
174
  ]
175
  },
 
205
  "### 8. Measure it against the base model\n",
206
  "\n",
207
  "Compare perplexity on held-out arXiv ML text. If stage A worked, Vashtra should be\n",
208
+ "meaningfully lower than Qwen3-0.6B here. Put the real number in the model card.\n",
209
+ "This is the claim that makes the model credible."
210
  ]
211
  },
212
  {
 
250
  "### 9. Export to ONNX for the in-browser demo\n",
251
  "\n",
252
  "Produces `vigneshwar234/Vashtra-0.6B-ONNX`, which the static WebGPU Space loads\n",
253
+ "directly, so the demo costs nothing to host and runs on the visitor's own GPU."
254
  ]
255
  },
256
  {
 
279
  "\n",
280
  "**Last step:** put the real perplexity number from cell 8 into the model card, and\n",
281
  "replace the \"weights are training\" banner. Measured results are what make people\n",
282
+ "trust a small model, so ship them.\n"
283
  ]
284
  }
285
  ],
build_corpus.py CHANGED
@@ -1,16 +1,21 @@
1
  """
2
  Build the Vashtra training corpus and push it to the Hub.
3
 
4
- Runs on Colab (fast network, plenty of disk). Produces two files in
5
- vigneshwar234/vashtra-ml-corpus:
6
 
7
- cpt.jsonl {"text": ...} raw ML literature for continued pretraining
8
- sft.jsonl {"messages": [..]} chat data for instruction tuning
9
 
10
  Sources
11
- CShorten/ML-ArXiv-Papers 117k arXiv cs.LG titles + abstracts
12
- donfu/oa-stackexchange ML / stats / CS Q&A with accepted answers
13
- HuggingFaceTB/smol-smoltalk general chat, anchors against forgetting
 
 
 
 
 
14
  """
15
  import json, random, re, os, collections
16
  from datasets import load_dataset
@@ -20,36 +25,78 @@ OUT = "corpus"
20
  os.makedirs(OUT, exist_ok=True)
21
  REPO = "vigneshwar234/vashtra-ml-corpus"
22
 
23
- WS = re.compile(r"\s+")
24
- def clean(s: str) -> str:
25
- return WS.sub(" ", (s or "")).strip()
 
 
 
 
26
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- # ------------------------------------------------------------------ 1. arXiv
29
- print("[1/4] arXiv cs.LG …")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ax = load_dataset("CShorten/ML-ArXiv-Papers", split="train")
31
  papers, seen_title = [], set()
32
  for r in ax:
33
  t, a = clean(r.get("title")), clean(r.get("abstract"))
34
- if not t or not a:
35
  continue
36
- if not (300 < len(a) < 2200) or len(t) < 15:
 
37
  continue
38
- key = t.lower()[:90]
39
- if key in seen_title:
40
- continue
41
- seen_title.add(key)
42
  papers.append((t, a))
43
- print(f" usable papers: {len(papers)}")
 
 
 
 
 
 
44
 
45
- with open(f"{OUT}/cpt.jsonl", "w") as fh:
46
- for t, a in papers:
47
- fh.write(json.dumps({"text": f"# {t}\n\n{a}"}) + "\n")
48
- print(f" cpt.jsonl: {len(papers)} docs "
49
- f"(~{sum(len(t)+len(a) for t, a in papers)/4/1e6:.1f}M tokens)")
50
 
51
- # arXiv-derived instruction tasks. Varied phrasings so the model learns the
52
- # task rather than the template.
53
  EXPLAIN = [
54
  'Explain the main contribution of the paper "{t}".',
55
  'What problem does the paper "{t}" address, and how?',
@@ -63,7 +110,6 @@ TITLE = [
63
  "What would this machine-learning paper be titled?\n\n{a}",
64
  "Read this abstract and give the paper's title:\n\n{a}",
65
  ]
66
-
67
  sft, stats = [], collections.Counter()
68
  for i, (t, a) in enumerate(random.sample(papers, min(26000, len(papers)))):
69
  if i % 4 == 0:
@@ -72,12 +118,12 @@ for i, (t, a) in enumerate(random.sample(papers, min(26000, len(papers)))):
72
  u, r = random.choice(EXPLAIN).format(t=t), a
73
  sft.append({"messages": [{"role": "user", "content": u},
74
  {"role": "assistant", "content": r}]})
75
- stats["arxiv"] += 1
76
- print(f" arXiv SFT tasks: {stats['arxiv']}")
77
 
78
 
79
- # ---------------------------------------------------- 2. ML/CS StackExchange
80
- print("[2/4] StackExchange (ML / stats / CS) …")
81
  SITES = {
82
  "stackexchange-datascience", "stackexchange-ai", "stackexchange-stats",
83
  "stackexchange-cs", "stackexchange-cstheory",
@@ -85,8 +131,7 @@ SITES = {
85
  "stackexchange-crossvalidated",
86
  }
87
  se = load_dataset("donfu/oa-stackexchange", split="train", streaming=True)
88
- seen_q, n_se = set(), 0
89
- by_site = collections.Counter()
90
  for r in se:
91
  if r["SOURCE"] not in SITES:
92
  continue
@@ -96,56 +141,70 @@ for r in se:
96
  q, a = clean(r["INSTRUCTION"]), clean(r["RESPONSE"])
97
  if not (40 < len(q) < 1200) or not (80 < len(a) < 1600):
98
  continue
99
- key = q.lower()[:110]
100
- if key in seen_q:
101
  continue
102
- seen_q.add(key)
103
  sft.append({"messages": [{"role": "user", "content": q},
104
  {"role": "assistant", "content": a}]})
105
  by_site[r["SOURCE"]] += 1
106
- n_se += 1
107
- stats["stackexchange"] = n_se
108
- print(f" StackExchange SFT: {n_se}")
109
  for s, c in by_site.most_common():
110
  print(f" {c:>6} {s}")
111
 
112
 
113
- # ------------------------------------------------------- 3. general anchor
114
- print("[3/4] general-chat anchor …")
115
- anchor = load_dataset("HuggingFaceTB/smol-smoltalk", split="train", streaming=True)
116
- n_anchor = 0
117
- TARGET_ANCHOR = 20000
118
- for row in anchor:
119
- m = row["messages"]
120
- if not (2 <= len(m) <= 6):
121
- continue
122
- if sum(len(x["content"]) for x in m) > 3000:
123
- continue
124
- if m[0]["role"] not in ("user", "system"):
125
- continue
126
- sft.append({"messages": m})
127
- n_anchor += 1
128
- if n_anchor >= TARGET_ANCHOR:
129
- break
130
- stats["anchor"] = n_anchor
131
- print(f" anchor SFT: {n_anchor}")
132
-
133
-
134
- # ------------------------------------------------------------------ 4. write
135
- print("[4/4] shuffle + write …")
 
 
 
 
 
 
 
 
 
 
 
136
  random.shuffle(sft)
137
  with open(f"{OUT}/sft.jsonl", "w") as fh:
138
  for r in sft:
139
  fh.write(json.dumps(r) + "\n")
140
 
141
- print(f" sft.jsonl: {len(sft)} examples")
142
  for k, v in stats.most_common():
143
- print(f" {v:>6} {k} ({v/len(sft)*100:.1f}%)")
144
-
145
- with open(f"{OUT}/stats.json", "w") as fh:
146
- json.dump({"cpt_docs": len(papers), "sft_total": len(sft),
147
- "sft_breakdown": dict(stats),
148
- "stackexchange_by_site": dict(by_site)}, fh, indent=2)
 
 
 
 
149
 
150
  from huggingface_hub import HfApi
151
  api = HfApi()
 
1
  """
2
  Build the Vashtra training corpus and push it to the Hub.
3
 
4
+ Runs on Colab, where network and disk are not a constraint. Produces two files
5
+ in vigneshwar234/vashtra-ml-corpus:
6
 
7
+ cpt.jsonl {"text": ...} ML text for continued pretraining
8
+ sft.jsonl {"messages": [..]} chat data for instruction tuning
9
 
10
  Sources
11
+ common-pile/arxiv_papers_filtered full-text openly-licensed arXiv papers
12
+ CShorten/ML-ArXiv-Papers 117k arXiv cs.LG titles + abstracts
13
+ donfu/oa-stackexchange ML / stats / CS Q&A, accepted answers
14
+ HuggingFaceTB/smoltalk2 science reasoning, instruction following,
15
+ general chat anchor
16
+
17
+ Full text is the point. Abstracts teach the model what papers claim; full text
18
+ teaches it how the argument is actually made.
19
  """
20
  import json, random, re, os, collections
21
  from datasets import load_dataset
 
25
  os.makedirs(OUT, exist_ok=True)
26
  REPO = "vigneshwar234/vashtra-ml-corpus"
27
 
28
+ # How much full-text to keep. Each paper is truncated to CHARS_PER_PAPER, so
29
+ # the budget is predictable rather than dominated by a few 80-page papers.
30
+ FULLTEXT_PAPERS = 22000
31
+ CHARS_PER_PAPER = 12000
32
+ ANCHOR_TARGET = 22000
33
+ SCIENCE_TARGET = 30000
34
+ IFEVAL_TARGET = 12000
35
 
36
+ WS = re.compile(r"\s+")
37
+ def clean(s): return WS.sub(" ", (s or "")).strip()
38
+
39
+ # Terms that mark a paper as machine-learning rather than, say, cosmology.
40
+ ML_TERMS = [
41
+ "neural network", "deep learning", "machine learning", "gradient descent",
42
+ "training data", "transformer", "convolutional", "reinforcement learning",
43
+ "classifier", "supervised", "unsupervised", "embedding", "backpropagation",
44
+ "loss function", "overfitting", "attention mechanism", "language model",
45
+ "generalization", "stochastic gradient", "fine-tuning", "benchmark dataset",
46
+ ]
47
 
48
+ cpt_lines = 0
49
+ cpt_chars = 0
50
+ fh_cpt = open(f"{OUT}/cpt.jsonl", "w")
51
+
52
+
53
+ # ------------------------------------------- 1. full-text arXiv (the upgrade)
54
+ print("[1/5] full-text arXiv (common-pile) ...")
55
+ kept = 0
56
+ try:
57
+ ft = load_dataset("common-pile/arxiv_papers_filtered", split="train",
58
+ streaming=True)
59
+ for r in ft:
60
+ txt = r.get("text") or ""
61
+ if len(txt) < 4000:
62
+ continue
63
+ head = txt[:4000].lower()
64
+ score = sum(1 for t in ML_TERMS if t in head)
65
+ if score < 2: # not an ML paper
66
+ continue
67
+ body = clean(txt)[:CHARS_PER_PAPER]
68
+ fh_cpt.write(json.dumps({"text": body}) + "\n")
69
+ cpt_lines += 1; cpt_chars += len(body); kept += 1
70
+ if kept >= FULLTEXT_PAPERS:
71
+ break
72
+ print(f" full-text papers kept: {kept}")
73
+ except Exception as e:
74
+ print(f" full-text SKIPPED ({type(e).__name__}: {e})")
75
+
76
+
77
+ # --------------------------------------------------- 2. arXiv cs.LG abstracts
78
+ print("[2/5] arXiv cs.LG abstracts ...")
79
  ax = load_dataset("CShorten/ML-ArXiv-Papers", split="train")
80
  papers, seen_title = [], set()
81
  for r in ax:
82
  t, a = clean(r.get("title")), clean(r.get("abstract"))
83
+ if not t or not a or not (300 < len(a) < 2200) or len(t) < 15:
84
  continue
85
+ k = t.lower()[:90]
86
+ if k in seen_title:
87
  continue
88
+ seen_title.add(k)
 
 
 
89
  papers.append((t, a))
90
+ for t, a in papers:
91
+ doc = f"# {t}\n\n{a}"
92
+ fh_cpt.write(json.dumps({"text": doc}) + "\n")
93
+ cpt_lines += 1; cpt_chars += len(doc)
94
+ fh_cpt.close()
95
+ print(f" abstracts: {len(papers)}")
96
+ print(f" cpt.jsonl: {cpt_lines} docs, ~{cpt_chars/4/1e6:.0f}M tokens")
97
 
 
 
 
 
 
98
 
99
+ # ------------------------------------------------ 3. arXiv instruction tasks
 
100
  EXPLAIN = [
101
  'Explain the main contribution of the paper "{t}".',
102
  'What problem does the paper "{t}" address, and how?',
 
110
  "What would this machine-learning paper be titled?\n\n{a}",
111
  "Read this abstract and give the paper's title:\n\n{a}",
112
  ]
 
113
  sft, stats = [], collections.Counter()
114
  for i, (t, a) in enumerate(random.sample(papers, min(26000, len(papers)))):
115
  if i % 4 == 0:
 
118
  u, r = random.choice(EXPLAIN).format(t=t), a
119
  sft.append({"messages": [{"role": "user", "content": u},
120
  {"role": "assistant", "content": r}]})
121
+ stats["arxiv_tasks"] += 1
122
+ print(f"[3/5] arXiv SFT tasks: {stats['arxiv_tasks']}")
123
 
124
 
125
+ # ---------------------------------------------------- 4. ML/CS StackExchange
126
+ print("[4/5] StackExchange (ML / stats / CS) ...")
127
  SITES = {
128
  "stackexchange-datascience", "stackexchange-ai", "stackexchange-stats",
129
  "stackexchange-cs", "stackexchange-cstheory",
 
131
  "stackexchange-crossvalidated",
132
  }
133
  se = load_dataset("donfu/oa-stackexchange", split="train", streaming=True)
134
+ seen_q, by_site = set(), collections.Counter()
 
135
  for r in se:
136
  if r["SOURCE"] not in SITES:
137
  continue
 
141
  q, a = clean(r["INSTRUCTION"]), clean(r["RESPONSE"])
142
  if not (40 < len(q) < 1200) or not (80 < len(a) < 1600):
143
  continue
144
+ k = q.lower()[:110]
145
+ if k in seen_q:
146
  continue
147
+ seen_q.add(k)
148
  sft.append({"messages": [{"role": "user", "content": q},
149
  {"role": "assistant", "content": a}]})
150
  by_site[r["SOURCE"]] += 1
151
+ stats["stackexchange"] += 1
152
+ print(f" StackExchange SFT: {stats['stackexchange']}")
 
153
  for s, c in by_site.most_common():
154
  print(f" {c:>6} {s}")
155
 
156
 
157
+ # ---------------------------------------------------------- 5. smoltalk2 mix
158
+ def take(subset, target, label):
159
+ """Pull `target` conversations from a smoltalk2 subset."""
160
+ n = 0
161
+ try:
162
+ d = load_dataset("HuggingFaceTB/smoltalk2", subset, split="train",
163
+ streaming=True)
164
+ for row in d:
165
+ m = row.get("messages")
166
+ if not m or not (2 <= len(m) <= 6):
167
+ continue
168
+ if sum(len(x["content"]) for x in m) > 4000:
169
+ continue
170
+ if m[0]["role"] not in ("user", "system"):
171
+ continue
172
+ sft.append({"messages": m})
173
+ n += 1
174
+ if n >= target:
175
+ break
176
+ except Exception as e:
177
+ print(f" {label} SKIPPED ({type(e).__name__}: {e})")
178
+ stats[label] = n
179
+ print(f" {label}: {n}")
180
+ return n
181
+
182
+ print("[5/5] smoltalk2 ...")
183
+ take("SFT/Mixture_of_Thoughts_science_no_think", SCIENCE_TARGET, "science_reasoning")
184
+ take("SFT/tulu_3_sft_personas_instruction_following_no_think", IFEVAL_TARGET,
185
+ "instruction_following")
186
+ take("SFT/smoltalk_smollm3_smol_magpie_ultra_no_think", ANCHOR_TARGET, "general_anchor")
187
+
188
+
189
+ # ------------------------------------------------------------------- write
190
+ print("shuffle + write ...")
191
  random.shuffle(sft)
192
  with open(f"{OUT}/sft.jsonl", "w") as fh:
193
  for r in sft:
194
  fh.write(json.dumps(r) + "\n")
195
 
196
+ print(f"sft.jsonl: {len(sft)} examples")
197
  for k, v in stats.most_common():
198
+ print(f" {v:>7} {k} ({v/len(sft)*100:.1f}%)")
199
+
200
+ json.dump({"cpt_docs": cpt_lines,
201
+ "cpt_approx_tokens": int(cpt_chars / 4),
202
+ "fulltext_papers": kept,
203
+ "abstracts": len(papers),
204
+ "sft_total": len(sft),
205
+ "sft_breakdown": dict(stats),
206
+ "stackexchange_by_site": dict(by_site)},
207
+ open(f"{OUT}/stats.json", "w"), indent=2)
208
 
209
  from huggingface_hub import HfApi
210
  api = HfApi()
train_vashtra.py CHANGED
@@ -7,10 +7,15 @@ Vashtra-0.6B - two-stage domain adaptation of Qwen3-0.6B for machine learning.
7
  Full-parameter fine-tune with 8-bit Adam. At 0.6B this beats LoRA for domain
8
  adaptation and still fits a free Colab T4 (~10GB of 16GB).
9
 
 
 
 
 
10
  Run:
11
- python train_vashtra.py # both stages, push to Hub
12
- python train_vashtra.py --stage sft # SFT only, from base
13
- python train_vashtra.py --cpt_docs 40000 # shorter stage A
 
14
  """
15
  import argparse, os, json
16
  import torch
@@ -28,11 +33,16 @@ SEED = 17
28
  def parse():
29
  p = argparse.ArgumentParser()
30
  p.add_argument("--stage", choices=["all", "cpt", "sft"], default="all")
31
- p.add_argument("--cpt_docs", type=int, default=0, help="0 = all")
 
 
 
32
  p.add_argument("--sft_examples", type=int, default=0, help="0 = all")
33
  p.add_argument("--bs", type=int, default=8)
34
  p.add_argument("--accum", type=int, default=4)
35
  p.add_argument("--sft_epochs", type=float, default=2.0)
 
 
36
  p.add_argument("--no_push", action="store_true")
37
  return p.parse_args()
38
 
@@ -92,9 +102,15 @@ def load_model(path):
92
  return m
93
 
94
 
95
- def targs(out, epochs, lr, args):
 
 
 
 
 
96
  return TrainingArguments(
97
  output_dir=out,
 
98
  per_device_train_batch_size=args.bs,
99
  gradient_accumulation_steps=args.accum,
100
  num_train_epochs=epochs,
@@ -117,12 +133,38 @@ def targs(out, epochs, lr, args):
117
  )
118
 
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  # --------------------------------------------------------------- Stage A: CPT
121
  def stage_cpt(args, out="vashtra-cpt"):
122
  ds = load_dataset(CORPUS, data_files="cpt.jsonl", split="train")
123
- if args.cpt_docs:
124
- ds = ds.select(range(min(args.cpt_docs, len(ds))))
125
- print(f"[cpt] documents: {len(ds)}")
126
 
127
  eos = tok.eos_token_id
128
 
@@ -142,11 +184,23 @@ def stage_cpt(args, out="vashtra-cpt"):
142
 
143
  ds = ds.map(pack, batched=True, batch_size=2000,
144
  remove_columns=ds.column_names, num_proc=2)
145
- print(f"[cpt] packed blocks: {len(ds)} (~{len(ds)*SEQ/1e6:.1f}M tokens)")
146
-
 
 
 
 
 
 
 
 
 
 
 
147
  model = load_model(BASE_MODEL)
148
- Trainer(model=model, args=targs(out, 1, 5e-5, args),
149
- train_dataset=ds, data_collator=pad_collate).train()
 
150
  model.save_pretrained(out); tok.save_pretrained(out)
151
  print(f"[cpt] saved -> {out}")
152
  return out
@@ -164,9 +218,13 @@ def stage_sft(args, init_path, out="vashtra-sft"):
164
  ds = ds.filter(lambda r: any(l != -100 for l in r["labels"]), num_proc=2)
165
  print(f"[sft] with supervised tokens: {len(ds)}")
166
 
 
 
167
  model = load_model(init_path)
168
- Trainer(model=model, args=targs(out, args.sft_epochs, 1e-5, args),
169
- train_dataset=ds, data_collator=pad_collate).train()
 
 
170
  model.save_pretrained(out); tok.save_pretrained(out)
171
  print(f"[sft] saved -> {out}")
172
  return out
 
7
  Full-parameter fine-tune with 8-bit Adam. At 0.6B this beats LoRA for domain
8
  adaptation and still fits a free Colab T4 (~10GB of 16GB).
9
 
10
+ The corpus is larger than one free Colab session can train on, so stage A takes
11
+ a token budget. Checkpoints are pushed to the Hub, so --resume continues a run
12
+ that was cut short in an earlier session.
13
+
14
  Run:
15
+ python train_vashtra.py # both stages, push to Hub
16
+ python train_vashtra.py --stage sft # SFT only, from base
17
+ python train_vashtra.py --cpt_tokens 60000000 # longer stage A
18
+ python train_vashtra.py --resume # continue a dropped run
19
  """
20
  import argparse, os, json
21
  import torch
 
33
  def parse():
34
  p = argparse.ArgumentParser()
35
  p.add_argument("--stage", choices=["all", "cpt", "sft"], default="all")
36
+ p.add_argument("--cpt_tokens", type=int, default=30_000_000,
37
+ help="token budget for stage A; 0 = use the whole corpus. "
38
+ "The corpus is ~90M tokens, which is more than one free "
39
+ "Colab session can finish. 30M is about 2 hours on a T4.")
40
  p.add_argument("--sft_examples", type=int, default=0, help="0 = all")
41
  p.add_argument("--bs", type=int, default=8)
42
  p.add_argument("--accum", type=int, default=4)
43
  p.add_argument("--sft_epochs", type=float, default=2.0)
44
+ p.add_argument("--resume", action="store_true",
45
+ help="resume from the last checkpoint pushed to the Hub")
46
  p.add_argument("--no_push", action="store_true")
47
  return p.parse_args()
48
 
 
102
  return m
103
 
104
 
105
+ def targs(out, epochs, lr, args, hub_id=None):
106
+ """Checkpoints go to the Hub so a dropped Colab session can be resumed."""
107
+ hub = {}
108
+ if hub_id and not args.no_push:
109
+ hub = dict(push_to_hub=True, hub_model_id=hub_id,
110
+ hub_strategy="checkpoint", hub_private_repo=False)
111
  return TrainingArguments(
112
  output_dir=out,
113
+ **hub,
114
  per_device_train_batch_size=args.bs,
115
  gradient_accumulation_steps=args.accum,
116
  num_train_epochs=epochs,
 
133
  )
134
 
135
 
136
+ def fetch_checkpoint(hub_id, out):
137
+ """Pull `last-checkpoint` from the Hub into out/ so training can resume.
138
+
139
+ Trainer's resume_from_checkpoint only looks on local disk. A fresh Colab
140
+ session has none, so without this a dropped run restarts from zero.
141
+ Returns the local checkpoint path, or None if there is nothing to resume.
142
+ """
143
+ import glob
144
+ from huggingface_hub import snapshot_download
145
+
146
+ local = glob.glob(os.path.join(out, "checkpoint-*")) or \
147
+ glob.glob(os.path.join(out, "last-checkpoint"))
148
+ if local:
149
+ print(f"[resume] using local checkpoint {local[0]}")
150
+ return sorted(local)[-1]
151
+ try:
152
+ p = snapshot_download(repo_id=hub_id, allow_patterns="last-checkpoint/*",
153
+ local_dir=out)
154
+ p = os.path.join(p, "last-checkpoint")
155
+ if os.path.isdir(p):
156
+ print(f"[resume] downloaded checkpoint from {hub_id}")
157
+ return p
158
+ except Exception as e:
159
+ print(f"[resume] nothing to resume from {hub_id} ({type(e).__name__})")
160
+ return None
161
+
162
+
163
  # --------------------------------------------------------------- Stage A: CPT
164
  def stage_cpt(args, out="vashtra-cpt"):
165
  ds = load_dataset(CORPUS, data_files="cpt.jsonl", split="train")
166
+ ds = ds.shuffle(seed=SEED) # mix full-text papers and abstracts together
167
+ print(f"[cpt] documents available: {len(ds)}")
 
168
 
169
  eos = tok.eos_token_id
170
 
 
184
 
185
  ds = ds.map(pack, batched=True, batch_size=2000,
186
  remove_columns=ds.column_names, num_proc=2)
187
+ print(f"[cpt] corpus packs to {len(ds)} blocks (~{len(ds)*SEQ/1e6:.0f}M tokens)")
188
+
189
+ if args.cpt_tokens:
190
+ keep = min(len(ds), args.cpt_tokens // SEQ)
191
+ if keep < len(ds):
192
+ print(f"[cpt] budget {args.cpt_tokens/1e6:.0f}M tokens -> training on "
193
+ f"{keep} of {len(ds)} blocks. Raise --cpt_tokens (or set 0) to "
194
+ f"use the whole corpus over multiple sessions.")
195
+ ds = ds.select(range(keep))
196
+ print(f"[cpt] training on {len(ds)} blocks (~{len(ds)*SEQ/1e6:.1f}M tokens)")
197
+
198
+ ckpt_repo = f"{HUB_MODEL}-cpt-ckpt"
199
+ resume = fetch_checkpoint(ckpt_repo, out) if args.resume else None
200
  model = load_model(BASE_MODEL)
201
+ Trainer(model=model, args=targs(out, 1, 5e-5, args, hub_id=ckpt_repo),
202
+ train_dataset=ds, data_collator=pad_collate
203
+ ).train(resume_from_checkpoint=resume)
204
  model.save_pretrained(out); tok.save_pretrained(out)
205
  print(f"[cpt] saved -> {out}")
206
  return out
 
218
  ds = ds.filter(lambda r: any(l != -100 for l in r["labels"]), num_proc=2)
219
  print(f"[sft] with supervised tokens: {len(ds)}")
220
 
221
+ ckpt_repo = f"{HUB_MODEL}-sft-ckpt"
222
+ resume = fetch_checkpoint(ckpt_repo, out) if args.resume else None
223
  model = load_model(init_path)
224
+ Trainer(model=model, args=targs(out, args.sft_epochs, 1e-5, args,
225
+ hub_id=ckpt_repo),
226
+ train_dataset=ds, data_collator=pad_collate
227
+ ).train(resume_from_checkpoint=resume)
228
  model.save_pretrained(out); tok.save_pretrained(out)
229
  print(f"[sft] saved -> {out}")
230
  return out