Skip to content

Commit efcf46e

Browse files
JuliaEdomclaude
andcommitted
feat(autotune): Colab serve notebook — bnb-4bit base + LoRA on a single T4
fp16 7B does not fit Colab's single T4, but the adapter was trained on the bnb-4bit base, so 4-bit serving is the training-time base, not a confound — and base-vs-tuned on the same quantization is the clean ablation. vLLM bitsandbytes + LoRA (sqltuned), smoke of both served names before exposing the cloudflared URL, relay via the private nlsql-tunnel dataset. Unblocks the student eval without waiting for the Kaggle weekly quota reset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 461a10c commit efcf46e

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
{
2+
"nbformat": 4,
3+
"nbformat_minor": 5,
4+
"metadata": {
5+
"colab": {
6+
"provenance": []
7+
},
8+
"kernelspec": {
9+
"display_name": "Python 3",
10+
"name": "python3"
11+
},
12+
"language_info": {
13+
"name": "python"
14+
},
15+
"accelerator": "GPU"
16+
},
17+
"cells": [
18+
{
19+
"cell_type": "markdown",
20+
"metadata": {},
21+
"source": [
22+
"# NL_SQL autotune — Colab SERVE (tuned student behind a public URL)\n",
23+
"\n",
24+
"Runs vLLM with the QLoRA adapter on a single T4 and exposes it via a cloudflared\n",
25+
"quick tunnel, so the Windows-side harness can eval `sqltuned` and the base model.\n",
26+
"\n",
27+
"Why bnb-4bit: fp16 7B (~15GB) does not fit a 16GB T4; the adapter was trained on\n",
28+
"`unsloth/qwen2.5-coder-7b-instruct-bnb-4bit`, so 4-bit serving matches the\n",
29+
"training-time base exactly.\n",
30+
"\n",
31+
"Steps: Runtime -> Change runtime type -> **T4 GPU** -> Run all. Two prompts:\n",
32+
"Drive authorisation, and (first time only) `kaggle.json` upload\n",
33+
"(Kaggle -> Settings -> Create New API Token). Then leave the tab open.\n",
34+
"The tunnel URL is printed below the last cell AND pushed to the private\n",
35+
"Kaggle dataset `liovinajo/nlsql-tunnel` for the Windows side to poll.\n"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": null,
41+
"metadata": {},
42+
"outputs": [],
43+
"source": [
44+
"BASE_MODEL = \"unsloth/qwen2.5-coder-7b-instruct-bnb-4bit\" # training-time base, pre-quantized\n",
45+
"ADAPTER_DATASET = \"liovinajo/nlsql-adapter\"\n",
46+
"\n",
47+
"DRIVE_DIR = \"/content/drive/MyDrive/nlsql_autotune\" # kaggle.json persists here\n",
48+
"ADAPTER_LOCAL = \"/content/nlsql_adapter\"\n",
49+
"SERVE_HOURS = 8.0\n",
50+
"PORT = 8000\n"
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": null,
56+
"metadata": {},
57+
"outputs": [],
58+
"source": [
59+
"import subprocess\n",
60+
"from pathlib import Path\n",
61+
"\n",
62+
"from google.colab import drive, files\n",
63+
"\n",
64+
"print(subprocess.run([\"nvidia-smi\"], capture_output=True, text=True).stdout, flush=True)\n",
65+
"\n",
66+
"drive.mount(\"/content/drive\")\n",
67+
"Path(DRIVE_DIR).mkdir(parents=True, exist_ok=True)\n",
68+
"\n",
69+
"# kaggle.json: reuse the copy on Drive if a previous session left one, else ask once.\n",
70+
"cred = Path(\"/root/.kaggle/kaggle.json\")\n",
71+
"cred.parent.mkdir(parents=True, exist_ok=True)\n",
72+
"drive_cred = Path(DRIVE_DIR) / \"kaggle.json\"\n",
73+
"if drive_cred.exists():\n",
74+
" cred.write_bytes(drive_cred.read_bytes())\n",
75+
" print(\"kaggle.json taken from Drive\", flush=True)\n",
76+
"else:\n",
77+
" print(\"upload kaggle.json (Kaggle -> Settings -> Create New API Token)\", flush=True)\n",
78+
" up = files.upload()\n",
79+
" cred.write_bytes(next(iter(up.values())))\n",
80+
" drive_cred.write_bytes(cred.read_bytes())\n",
81+
"cred.chmod(0o600)\n"
82+
]
83+
},
84+
{
85+
"cell_type": "code",
86+
"execution_count": null,
87+
"metadata": {},
88+
"outputs": [],
89+
"source": [
90+
"import shutil\n",
91+
"import subprocess\n",
92+
"from pathlib import Path\n",
93+
"\n",
94+
"dst = Path(ADAPTER_LOCAL)\n",
95+
"if dst.exists():\n",
96+
" shutil.rmtree(dst)\n",
97+
"dst.mkdir(parents=True)\n",
98+
"subprocess.run(\n",
99+
" [\"kaggle\", \"datasets\", \"download\", ADAPTER_DATASET, \"-p\", str(dst), \"--unzip\"],\n",
100+
" check=True,\n",
101+
")\n",
102+
"# Kaggle unpacks uploaded zips flat (v10 lesson) -- self-home on the config.\n",
103+
"hits = sorted(dst.rglob(\"adapter_config.json\"))\n",
104+
"if not hits:\n",
105+
" raise RuntimeError(f\"no adapter_config.json under {dst} -- wrong dataset?\")\n",
106+
"adapter_dir = str(hits[0].parent)\n",
107+
"if not Path(adapter_dir, \"adapter_model.safetensors\").exists():\n",
108+
" raise RuntimeError(f\"adapter_model.safetensors missing next to {hits[0]}\")\n",
109+
"print(f\"adapter_dir resolved: {adapter_dir}\", flush=True)\n"
110+
]
111+
},
112+
{
113+
"cell_type": "code",
114+
"execution_count": null,
115+
"metadata": {},
116+
"outputs": [],
117+
"source": [
118+
"import json\n",
119+
"import os\n",
120+
"import re\n",
121+
"import subprocess\n",
122+
"import sys\n",
123+
"import time\n",
124+
"import urllib.request\n",
125+
"from pathlib import Path\n",
126+
"\n",
127+
"subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"vllm>=0.6.3\"], check=True)\n",
128+
"cf = \"/content/cloudflared\"\n",
129+
"if not Path(cf).exists():\n",
130+
" urllib.request.urlretrieve(\n",
131+
" \"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64\",\n",
132+
" cf,\n",
133+
" )\n",
134+
" subprocess.run([\"chmod\", \"+x\", cf], check=True)\n",
135+
"\n",
136+
"vllm_cmd = [\n",
137+
" \"vllm\",\n",
138+
" \"serve\",\n",
139+
" BASE_MODEL,\n",
140+
" \"--port\",\n",
141+
" str(PORT),\n",
142+
" \"--quantization\",\n",
143+
" \"bitsandbytes\",\n",
144+
" \"--load-format\",\n",
145+
" \"bitsandbytes\",\n",
146+
" \"--gpu-memory-utilization\",\n",
147+
" \"0.92\",\n",
148+
" \"--max-model-len\",\n",
149+
" \"12288\",\n",
150+
" \"--enable-lora\",\n",
151+
" \"--max-lora-rank\",\n",
152+
" \"16\",\n",
153+
" \"--lora-modules\",\n",
154+
" f\"sqltuned={adapter_dir}\",\n",
155+
"]\n",
156+
"print(\" \".join(vllm_cmd), flush=True)\n",
157+
"vllm_log = open(\"/content/vllm.log\", \"w\")\n",
158+
"vllm_proc = subprocess.Popen(vllm_cmd, stdout=vllm_log, stderr=subprocess.STDOUT)\n",
159+
"\n",
160+
"print(\"waiting for vllm (первый старт качает ~5.5GB весов)...\", flush=True)\n",
161+
"for _ in range(240): # up to 40 min\n",
162+
" time.sleep(10)\n",
163+
" try:\n",
164+
" urllib.request.urlopen(f\"http://127.0.0.1:{PORT}/v1/models\", timeout=5)\n",
165+
" break\n",
166+
" except Exception:\n",
167+
" if vllm_proc.poll() is not None:\n",
168+
" print(Path(\"/content/vllm.log\").read_text()[-4000:])\n",
169+
" raise RuntimeError(\"vllm died during startup\") from None\n",
170+
"else:\n",
171+
" raise RuntimeError(\"vllm never became ready\")\n",
172+
"\n",
173+
"# smoke BOTH served names before exposing the URL\n",
174+
"for name in (\"sqltuned\", BASE_MODEL):\n",
175+
" body = json.dumps(\n",
176+
" {\n",
177+
" \"model\": name,\n",
178+
" \"messages\": [{\"role\": \"user\", \"content\": \"Reply with the word ok.\"}],\n",
179+
" \"max_tokens\": 5,\n",
180+
" }\n",
181+
" ).encode()\n",
182+
" req = urllib.request.Request(\n",
183+
" f\"http://127.0.0.1:{PORT}/v1/chat/completions\",\n",
184+
" data=body,\n",
185+
" headers={\"Content-Type\": \"application/json\"},\n",
186+
" )\n",
187+
" with urllib.request.urlopen(req, timeout=120) as r:\n",
188+
" out = json.loads(r.read())[\"choices\"][0][\"message\"][\"content\"]\n",
189+
" print(f\"smoke {name}: {out!r}\", flush=True)\n",
190+
"\n",
191+
"tun = subprocess.Popen(\n",
192+
" [cf, \"tunnel\", \"--url\", f\"http://127.0.0.1:{PORT}\"],\n",
193+
" stdout=subprocess.PIPE,\n",
194+
" stderr=subprocess.STDOUT,\n",
195+
" text=True,\n",
196+
")\n",
197+
"url = None\n",
198+
"assert tun.stdout is not None\n",
199+
"for line in tun.stdout:\n",
200+
" m = re.search(r\"https://[a-z0-9-]+\\.trycloudflare\\.com\", line)\n",
201+
" if m:\n",
202+
" url = m.group(0)\n",
203+
" break\n",
204+
"print(f\"\\n{'=' * 60}\\nTUNNEL: {url}\\nbase_url for .env: {url}/v1\\n{'=' * 60}\\n\", flush=True)\n",
205+
"\n",
206+
"# push the URL out through the private nlsql-tunnel dataset (Windows polls it)\n",
207+
"relay = Path(\"/content/relay\")\n",
208+
"relay.mkdir(exist_ok=True)\n",
209+
"(relay / \"tunnel_url.txt\").write_text(url or \"NONE\")\n",
210+
"(relay / \"dataset-metadata.json\").write_text(\n",
211+
" json.dumps(\n",
212+
" {\"title\": \"nlsql-tunnel\", \"id\": \"liovinajo/nlsql-tunnel\", \"licenses\": [{\"name\": \"CC0-1.0\"}]}\n",
213+
" )\n",
214+
")\n",
215+
"for verb in ([\"version\", \"-m\", \"tunnel-url\"], [\"create\"]):\n",
216+
" rr = subprocess.run(\n",
217+
" [\"kaggle\", \"datasets\", *verb, \"-p\", str(relay)], capture_output=True, text=True\n",
218+
" )\n",
219+
" print(f\"relay {verb[0]}: rc={rr.returncode}\", flush=True)\n",
220+
" if rr.returncode == 0:\n",
221+
" break\n",
222+
"else:\n",
223+
" print(\"relay FAILED; copy the URL above manually\", flush=True)\n",
224+
"\n",
225+
"deadline = time.time() + SERVE_HOURS * 3600\n",
226+
"while time.time() < deadline and vllm_proc.poll() is None:\n",
227+
" time.sleep(300)\n",
228+
" print(f\"alive, url={url}, {int((deadline - time.time()) / 60)} min left\", flush=True)\n",
229+
"print(\"serve window over\")\n"
230+
]
231+
}
232+
]
233+
}

0 commit comments

Comments
 (0)