AIencoder commited on
Commit
ae0b5a7
·
verified ·
1 Parent(s): 528af79

Create forgekit/kaggle_runner.py

Browse files
Files changed (1) hide show
  1. forgekit/kaggle_runner.py +267 -0
forgekit/kaggle_runner.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kaggle integration — push and run merge notebooks on free T4 GPUs."""
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ import requests
7
+ from typing import Optional
8
+
9
+
10
+ KAGGLE_API_URL = "https://www.kaggle.com/api/v1"
11
+
12
+
13
+ def _kaggle_headers(username: str, api_key: str) -> dict:
14
+ """Create auth headers for Kaggle API (Basic auth)."""
15
+ import base64
16
+ creds = base64.b64encode(f"{username}:{api_key}".encode()).decode()
17
+ return {
18
+ "Authorization": f"Basic {creds}",
19
+ "Content-Type": "application/json",
20
+ }
21
+
22
+
23
+ def push_and_run_kernel(
24
+ notebook_json: str,
25
+ kernel_title: str,
26
+ kaggle_username: str,
27
+ kaggle_key: str,
28
+ enable_gpu: bool = True,
29
+ enable_internet: bool = True,
30
+ ) -> dict:
31
+ """Push a notebook to Kaggle and auto-run it.
32
+
33
+ Args:
34
+ notebook_json: The notebook content as JSON string
35
+ kernel_title: Title for the Kaggle kernel
36
+ kaggle_username: Kaggle username
37
+ kaggle_key: Kaggle API key
38
+ enable_gpu: Enable T4 GPU (free tier)
39
+ enable_internet: Enable internet access (needed for HF downloads)
40
+
41
+ Returns:
42
+ dict with status, url, and any errors
43
+ """
44
+ if not kaggle_username or not kaggle_key:
45
+ return {
46
+ "success": False,
47
+ "error": (
48
+ "**Kaggle credentials required**\n\n"
49
+ "1. Go to [kaggle.com/settings](https://www.kaggle.com/settings)\n"
50
+ "2. Scroll to **API** section\n"
51
+ "3. Click **Create New Token** (downloads `kaggle.json`)\n"
52
+ "4. Copy your username and key from that file"
53
+ ),
54
+ }
55
+
56
+ # Clean the title into a valid slug
57
+ slug = kernel_title.lower().replace(" ", "-")
58
+ slug = "".join(c for c in slug if c.isalnum() or c == "-")[:50]
59
+ kernel_slug = f"{kaggle_username}/{slug}"
60
+
61
+ headers = _kaggle_headers(kaggle_username, kaggle_key)
62
+
63
+ # Prepare kernel push payload
64
+ # Kaggle API expects the notebook source as a string
65
+ push_data = {
66
+ "id": kernel_slug,
67
+ "title": kernel_title[:50],
68
+ "code_file_name": f"{slug}.ipynb",
69
+ "code_file_content": notebook_json,
70
+ "language": "python",
71
+ "kernel_type": "notebook",
72
+ "is_private": True,
73
+ "enable_gpu": enable_gpu,
74
+ "enable_internet": enable_internet,
75
+ "dataset_sources": [],
76
+ "competition_sources": [],
77
+ "kernel_sources": [],
78
+ "category_ids": [],
79
+ }
80
+
81
+ try:
82
+ # Push kernel (this also triggers execution)
83
+ resp = requests.post(
84
+ f"{KAGGLE_API_URL}/kernels/push",
85
+ headers=headers,
86
+ json=push_data,
87
+ timeout=30,
88
+ )
89
+
90
+ if resp.status_code == 200:
91
+ result = resp.json()
92
+ kernel_url = f"https://www.kaggle.com/code/{kernel_slug}"
93
+ return {
94
+ "success": True,
95
+ "url": kernel_url,
96
+ "edit_url": f"{kernel_url}/edit",
97
+ "message": (
98
+ f"**Kernel pushed and running!**\n\n"
99
+ f"Your merge is now executing on Kaggle's free T4 GPU.\n\n"
100
+ f"- **View & Edit:** [{kernel_slug}]({kernel_url}/edit)\n"
101
+ f"- **Status:** [Check output]({kernel_url})\n\n"
102
+ f"The kernel will run automatically. Check back in ~15-30 min for 7B models.\n\n"
103
+ f"*Tip: Kaggle gives you 30 hours/week of free GPU time.*"
104
+ ),
105
+ "ref": result.get("ref", ""),
106
+ "version": result.get("versionNumber", 1),
107
+ }
108
+
109
+ elif resp.status_code == 401:
110
+ return {
111
+ "success": False,
112
+ "error": "Invalid Kaggle credentials. Check your username and API key.",
113
+ }
114
+ elif resp.status_code == 403:
115
+ return {
116
+ "success": False,
117
+ "error": "Kaggle API access forbidden. Make sure your API token has kernel permissions.",
118
+ }
119
+ else:
120
+ error_detail = ""
121
+ try:
122
+ error_detail = resp.json().get("message", resp.text[:200])
123
+ except Exception:
124
+ error_detail = resp.text[:200]
125
+ return {
126
+ "success": False,
127
+ "error": f"Kaggle API error ({resp.status_code}): {error_detail}",
128
+ }
129
+
130
+ except requests.exceptions.Timeout:
131
+ return {"success": False, "error": "Request timed out. Try again."}
132
+ except Exception as e:
133
+ return {"success": False, "error": f"Error: {str(e)}"}
134
+
135
+
136
+ def check_kernel_status(
137
+ kernel_slug: str,
138
+ kaggle_username: str,
139
+ kaggle_key: str,
140
+ ) -> dict:
141
+ """Check the execution status of a Kaggle kernel.
142
+
143
+ Args:
144
+ kernel_slug: Full kernel slug (username/kernel-name)
145
+ kaggle_username: Kaggle username
146
+ kaggle_key: Kaggle API key
147
+
148
+ Returns:
149
+ dict with status info
150
+ """
151
+ headers = _kaggle_headers(kaggle_username, kaggle_key)
152
+
153
+ try:
154
+ resp = requests.get(
155
+ f"{KAGGLE_API_URL}/kernels/status",
156
+ headers=headers,
157
+ params={"userName": kernel_slug.split("/")[0], "kernelSlug": kernel_slug.split("/")[1]},
158
+ timeout=15,
159
+ )
160
+
161
+ if resp.status_code == 200:
162
+ data = resp.json()
163
+ status = data.get("status", "unknown")
164
+
165
+ status_emoji = {
166
+ "queued": "⏳",
167
+ "running": "🔄",
168
+ "complete": "✅",
169
+ "error": "❌",
170
+ "cancelAcknowledged": "🚫",
171
+ }.get(status, "❓")
172
+
173
+ return {
174
+ "success": True,
175
+ "status": status,
176
+ "display": f"{status_emoji} **{status.upper()}**",
177
+ "failure_message": data.get("failureMessage", ""),
178
+ }
179
+ else:
180
+ return {"success": False, "error": f"API error: {resp.status_code}"}
181
+
182
+ except Exception as e:
183
+ return {"success": False, "error": str(e)}
184
+
185
+
186
+ def generate_kaggle_notebook(
187
+ merge_notebook: dict,
188
+ hf_token_secret: bool = True,
189
+ ) -> str:
190
+ """Adapt a merge notebook for Kaggle execution.
191
+
192
+ Modifies the notebook to:
193
+ - Use Kaggle's GPU environment
194
+ - Reference HF token from Kaggle secrets (if enabled)
195
+ - Add Kaggle-specific output handling
196
+
197
+ Args:
198
+ merge_notebook: The notebook dict from notebook_generator
199
+ hf_token_secret: If True, use Kaggle Secrets for HF token
200
+
201
+ Returns:
202
+ Notebook as JSON string
203
+ """
204
+ nb = json.loads(json.dumps(merge_notebook)) # deep copy
205
+
206
+ # Add Kaggle environment setup cell at the beginning (after the header)
207
+ kaggle_setup = {
208
+ "cell_type": "code",
209
+ "metadata": {},
210
+ "source": [
211
+ "# Kaggle Environment Setup\n",
212
+ "import os\n",
213
+ "\n",
214
+ "# Use Kaggle Secrets for HF token (add in Kaggle Settings > Secrets)\n",
215
+ "from kaggle_secrets import UserSecretsClient\n",
216
+ "try:\n",
217
+ " secrets = UserSecretsClient()\n",
218
+ " hf_token = secrets.get_secret('HF_TOKEN')\n",
219
+ " os.environ['HF_TOKEN'] = hf_token\n",
220
+ " os.environ['HUGGING_FACE_HUB_TOKEN'] = hf_token\n",
221
+ " print('✅ HF Token loaded from Kaggle Secrets')\n",
222
+ "except Exception:\n",
223
+ " print('⚠️ No HF_TOKEN secret found. Add it in Settings > Secrets if needed.')\n",
224
+ "\n",
225
+ "# Verify GPU\n",
226
+ "import torch\n",
227
+ "if torch.cuda.is_available():\n",
228
+ " print(f'✅ GPU: {torch.cuda.get_device_name(0)}')\n",
229
+ " print(f' VRAM: {torch.cuda.get_device_properties(0).total_mem / 1024**3:.1f} GB')\n",
230
+ "else:\n",
231
+ " print('⚠️ No GPU detected. Enable GPU in kernel settings.')\n",
232
+ ],
233
+ "outputs": [],
234
+ "execution_count": None,
235
+ }
236
+
237
+ # Insert after the first markdown cell (header)
238
+ if len(nb["cells"]) > 0:
239
+ nb["cells"].insert(1, kaggle_setup)
240
+
241
+ # Replace the HF login cell (notebook_login doesn't work on Kaggle)
242
+ for i, cell in enumerate(nb["cells"]):
243
+ if cell["cell_type"] == "code":
244
+ source = "".join(cell["source"]) if isinstance(cell["source"], list) else cell["source"]
245
+ if "notebook_login" in source:
246
+ nb["cells"][i]["source"] = [
247
+ "# HF Authentication (using Kaggle Secrets)\n",
248
+ "from huggingface_hub import login\n",
249
+ "import os\n",
250
+ "\n",
251
+ "hf_token = os.environ.get('HF_TOKEN', '')\n",
252
+ "if hf_token:\n",
253
+ " login(token=hf_token)\n",
254
+ " print('✅ Logged in to HuggingFace Hub')\n",
255
+ "else:\n",
256
+ " print('⚠️ No HF token. Add HF_TOKEN to Kaggle Secrets for gated models.')\n",
257
+ ]
258
+
259
+ # Update metadata for Kaggle
260
+ nb["metadata"]["kaggle"] = {
261
+ "accelerator": "gpu",
262
+ "dataSources": [],
263
+ "isGpuEnabled": True,
264
+ "isInternetEnabled": True,
265
+ }
266
+
267
+ return json.dumps(nb, indent=2, ensure_ascii=False)