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