tchbcb commited on
Commit
9d4d4ab
·
verified ·
1 Parent(s): 8927afe

oneclick_gsqrco_serve: one-command GSQ-RCO serving (tunnel-aware)

Browse files
Files changed (1) hide show
  1. oneclick_gsqrco_serve.py +293 -0
oneclick_gsqrco_serve.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """oneclick_gsqrco_serve.py — SAMAI-9B 最优量化模型一键 serving(GSQ-RCO-lite, R14)
4
+
5
+ 用法(Kaggle/Colab T4 容器内):
6
+ HF_TOKEN=hf_xxx python3 oneclick_gsqrco_serve.py
7
+ python3 oneclick_gsqrco_serve.py --token hf_xxx
8
+
9
+ 自动完成:
10
+ 1. HF 拉权重: m13_r10_gsqrco_v1.gguf(6.48G, ppl 7.99 vs 旧 mixbit 8.11)
11
+ + mmproj_m11.gguf(921M) + llama.cpp CUDA tar(sm_75)
12
+ 2. llama-server: 端口 8101, key=1234, ctx 梯子 131072(双卡)/65536(单卡)→32768,
13
+ -fa on -ctk q8_0 -ctv q8_0 -t 2 -ngl 99
14
+ 3. 公网隧道(二选一,自动识别):
15
+ a. 容器有 kaggle_server.py → 打 /v1 代理补丁后重启它(隧道原生出 OpenAI API,
16
+ 单隧道双功能; 坑#67 同机单身份/#68 监督环——勿抢端口, 补丁本体)
17
+ b. 无 → pip 装 aitun 并起免 token 隧道 -p 8101, 解析打印 aitun.cc URL
18
+ 4. 健康检查 + 打印调用示例
19
+
20
+ 日志: /tmp/k8b/oneclick.log 旗标: ONECLICK_DONE / ONECLICK_FAIL
21
+ """
22
+ import json, os, re, subprocess, sys, time, traceback
23
+
24
+ K8B, LCF = "/tmp/k8b", "/tmp/lcfin"
25
+ REPO = "tchbcb/samai-9b"
26
+ GGUF = K8B + "/m13_r10_gsqrco_v1.gguf"
27
+ MMP = K8B + "/mmproj_m11.gguf"
28
+ LOG = K8B + "/oneclick.log"
29
+ os.makedirs(K8B, exist_ok=True)
30
+ os.makedirs(LCF, exist_ok=True)
31
+ log = open(LOG, "a", buffering=1)
32
+
33
+
34
+ def P(m):
35
+ log.write("[%s] %s\n" % (time.strftime("%m-%d %H:%M:%S"), m))
36
+
37
+
38
+ def sh(c, t=3600):
39
+ p = subprocess.run(c, shell=True, capture_output=True, text=True,
40
+ timeout=t, errors="replace")
41
+ return (p.stdout or "") + (p.stderr or "")
42
+
43
+
44
+ def get_token(argv):
45
+ if "--token" in argv:
46
+ return argv[argv.index("--token") + 1]
47
+ if os.environ.get("HF_TOKEN"):
48
+ return os.environ["HF_TOKEN"]
49
+ p = os.path.expanduser("~/.cache/huggingface/token")
50
+ if os.path.exists(p):
51
+ return open(p).read().strip()
52
+ raise SystemExit("缺少 HF token: python3 oneclick_gsqrco_serve.py --token hf_xxx")
53
+
54
+
55
+ def hfget(tok, fn, dst, minb):
56
+ if os.path.exists(dst) and os.path.getsize(dst) >= minb:
57
+ P("have %s (%.1fG)" % (fn, os.path.getsize(dst) / 1e9))
58
+ return
59
+ from huggingface_hub import hf_hub_download
60
+ P("pull %s ..." % fn)
61
+ t0 = time.time()
62
+ p = hf_hub_download(REPO, fn, token=tok)
63
+ sh("cp -f '%s' '%s'" % (p, dst), 1800)
64
+ assert os.path.getsize(dst) >= minb, "pull short: " + fn
65
+ P("pulled %s %.1fG %.0fs" % (fn, os.path.getsize(dst) / 1e9, time.time() - t0))
66
+
67
+
68
+ def lc_setup(tok):
69
+ b = LCF + "/llama-server"
70
+ for tar_name in ("llama_cpp_cuda_colab_t4.tar.gz", "llama_cpp_cuda_t4_v2.tar.gz"):
71
+ if os.path.exists(b) and os.path.getsize(b) > 5e6:
72
+ v = sh("LD_LIBRARY_PATH=%s %s --version 2>&1 | head -1" % (LCF, b), 60)
73
+ if "version" in v.lower() or "build" in v.lower():
74
+ P("lcfin ok: %s" % v[-90:])
75
+ return True
76
+ TAR = K8B + "/lc_tar.tar.gz"
77
+ try:
78
+ hfget(tok, tar_name, TAR, 3e7)
79
+ except Exception as e:
80
+ P("tar %s fail: %s" % (tar_name, repr(e)[:100]))
81
+ continue
82
+ sh("rm -rf %s/* && tar xzf %s -C %s" % (LCF, TAR, LCF), 900)
83
+ sh('LS=$(find %s -name llama-server -type f | head -1); [ -n "$LS" ] && '
84
+ 'cp -f "$LS" %s/; find %s -name "*.so*" -type f -exec cp -f {} %s/ \\; '
85
+ '2>/dev/null; rm -f %s/libcuda.so* %s/libnvidia-*'
86
+ % (LCF, LCF, LCF, LCF, LCF, LCF), 300)
87
+ for cand in ("/usr/lib64-nvidia/libcuda.so.1",
88
+ "/usr/lib/x86_64-linux-gnu/libcuda.so.1",
89
+ "/usr/local/nvidia/lib64/libcuda.so.1"):
90
+ if os.path.exists(cand):
91
+ sh("ln -sf %s %s/libcuda.so.1" % (cand, LCF), 30)
92
+ break
93
+ return os.path.exists(b) and os.path.getsize(b) > 5e6
94
+
95
+
96
+ def n_gpu():
97
+ try:
98
+ return len([l for l in sh("nvidia-smi -L").strip().splitlines() if "GPU" in l])
99
+ except Exception:
100
+ return 1
101
+
102
+
103
+ def serve():
104
+ sh("pkill -f llama-server; sleep 2", 60)
105
+ ng = n_gpu()
106
+ env = ("LD_LIBRARY_PATH=%s:/usr/local/nvidia/lib64:/usr/lib64-nvidia:"
107
+ "/usr/local/cuda/lib64" % LCF)
108
+ import urllib.request
109
+ for c in (131072 if ng >= 2 else 65536, 32768):
110
+ cmd = ("%s %s/llama-server -m %s --mmproj %s --host 0.0.0.0 --port 8101 "
111
+ "--api-key 1234 -c %d -fa on -ctk q8_0 -ctv q8_0 -t 2 -ngl 99"
112
+ % (env, LCF, GGUF, MMP, c))
113
+ P("fire ctx=%d (ngpu=%d)" % (c, ng))
114
+ subprocess.Popen(["bash", "-c", cmd + " > /tmp/k8b/serve_oneclick.log 2>&1"],
115
+ stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
116
+ stdin=subprocess.DEVNULL, start_new_session=True)
117
+ for i in range(66):
118
+ time.sleep(5)
119
+ try:
120
+ h = urllib.request.urlopen("http://127.0.0.1:8101/health",
121
+ timeout=4).read().decode()
122
+ if '"ok"' in h:
123
+ P("SERVE_OK ctx=%d" % c)
124
+ return True
125
+ except Exception:
126
+ pass
127
+ if i in (12, 30):
128
+ t = sh("tail -3 /tmp/k8b/serve_oneclick.log")[-300:].lower()
129
+ if ("out of memory" in t) or ("cuda error" in t):
130
+ break
131
+ sh("pkill -f llama-server; sleep 2", 60)
132
+ return False
133
+
134
+
135
+ # ---- /v1 代理补丁文本(kaggle_server 模式; 坑#67/#68)----
136
+ V1_PATCH = '''
137
+ # ==== R14 BEGIN: /v1 proxy -> llama-server 8101 (oneclick) ====
138
+ import requests as _r14rq
139
+ _R14_LLM = "http://127.0.0.1:8101"
140
+
141
+ @app.route('/v1', methods=['GET', 'POST', 'OPTIONS'])
142
+ @app.route('/v1/<path:_r14p>', methods=['GET', 'POST', 'OPTIONS', 'PUT', 'PATCH', 'DELETE'])
143
+ def _r14_v1(_r14p=''):
144
+ _up = _R14_LLM + '/v1' + ('' if _r14p == '' else '/' + _r14p)
145
+ _h = {k: v for k, v in request.headers.items()
146
+ if k.lower() not in ('host', 'connection', 'accept-encoding', 'content-length')}
147
+ try:
148
+ _r = _r14rq.request(request.method, _up, params=request.args,
149
+ data=request.get_data(cache=False), headers=_h,
150
+ stream=True, timeout=(10, 600))
151
+ except Exception as _e:
152
+ return jsonify(error='llm upstream unreachable: %s' % repr(_e)[:120]), 502
153
+ _hd = {k: v for k, v in _r.headers.items()
154
+ if k.lower() not in ('content-length', 'transfer-encoding', 'connection',
155
+ 'content-encoding', 'keep-alive')}
156
+ if request.method == 'OPTIONS':
157
+ _r.close()
158
+ return ('', _r.status_code, _hd)
159
+ _ct = _r.headers.get('Content-Type', '')
160
+ if 'text/event-stream' in _ct or _r.headers.get('Transfer-Encoding', '').lower() == 'chunked':
161
+ def _r14gen():
162
+ try:
163
+ for _c in _r.iter_content(chunk_size=4096):
164
+ if _c:
165
+ yield _c
166
+ finally:
167
+ _r.close()
168
+ return Response(_r14gen(), status=_r.status_code, headers=_hd)
169
+ _body = _r.content
170
+ _r.close()
171
+ return Response(_body, status=_r.status_code, headers=_hd)
172
+ # ==== R14 END: /v1 proxy ====
173
+ '''
174
+
175
+
176
+ def find_ks():
177
+ for c in ("/kaggle/working/kaggle_server.py",
178
+ os.path.join(os.getcwd(), "kaggle_server.py")):
179
+ if os.path.exists(c):
180
+ return c
181
+ return None
182
+
183
+
184
+ def patch_kaggle_server(ks):
185
+ src = open(ks).read()
186
+ if "R14 BEGIN" in src:
187
+ P("kaggle_server already patched")
188
+ else:
189
+ m = re.search(r"^(\w+)\s*=\s*Flask\(", src, re.M)
190
+ assert m, "Flask app object not found"
191
+ anchor = "if __name__ == '__main__':"
192
+ assert anchor in src, "main anchor not found"
193
+ open(ks + ".bak_r14", "w").write(src)
194
+ src = src.replace(anchor, V1_PATCH + "\n\n" + anchor, 1)
195
+ compile(src, ks, "exec")
196
+ open(ks, "w").write(src)
197
+ P("kaggle_server patched (backup .bak_r14)")
198
+ return True
199
+
200
+
201
+ def restart_ksguarded(ks):
202
+ """杀 5000 持有者; 监督环优先重生, 8s 无重生则兜底自启(坑#68)"""
203
+ d = os.path.dirname(ks)
204
+ sh("KPID=$(ss -tlnp | grep ':5000 ' | grep -oP 'pid=\\K[0-9]+' | head -1); "
205
+ "[ -n \"$KPID\" ] && kill $KPID; sleep 6", 40)
206
+ for _ in range(4):
207
+ if ":5000 " in sh("ss -tlnp", 15):
208
+ break
209
+ time.sleep(3)
210
+ if not (":5000 " in sh("ss -tlnp", 15)):
211
+ subprocess.Popen(["bash", "-c",
212
+ "cd %s && nohup python3 kaggle_server.py "
213
+ ">> /tmp/k8b/oneclick_ks.log 2>&1 &" % d],
214
+ stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
215
+ stdin=subprocess.DEVNULL, start_new_session=True)
216
+ time.sleep(5)
217
+ P("kaggle_server restarted: %s" % (":5000 " in sh("ss -tlnp", 15)))
218
+
219
+
220
+ def tunnel_url_from(path):
221
+ try:
222
+ m = re.search(r"Proxy URL: (https?://\S+?)/?\s", open(path).read() + "\n")
223
+ return m.group(1) if m else None
224
+ except Exception:
225
+ return None
226
+
227
+
228
+ def main():
229
+ t0 = time.time()
230
+ P("==== oneclick start ====")
231
+ try:
232
+ tok = get_token(sys.argv)
233
+ hfhome = os.path.expanduser("~/.cache/huggingface")
234
+ os.makedirs(hfhome, exist_ok=True)
235
+ open(hfhome + "/token", "w").write(tok)
236
+ os.chmod(hfhome + "/token", 0o600)
237
+ hfget(tok, "m13_r10_gsqrco_v1.gguf", GGUF, 5e9)
238
+ hfget(tok, "mmproj_m11.gguf", MMP, 5e8)
239
+ if not lc_setup(tok):
240
+ raise RuntimeError("llama-server unavailable")
241
+ if not serve():
242
+ raise RuntimeError("serve failed at all ctx")
243
+ ks = find_ks()
244
+ pub = None
245
+ if ks:
246
+ patch_kaggle_server(ks)
247
+ restart_ksguarded(ks)
248
+ for lg in ("/tmp/k8b/aitun_main.log", "/tmp/k8b/aitun_8101.log"):
249
+ pub = tunnel_url_from(lg)
250
+ if pub:
251
+ break
252
+ print("\n[MODE] kaggle_server /v1 ���理(单隧道双功能)")
253
+ print("[API] <你的隧道URL>/v1 (沿用现有 aitun 隧道码)")
254
+ else:
255
+ sh("which aitun || pip install -q aitun || pip install -q "
256
+ "git+https://github.com/ctz168/aitun.git", 600)
257
+ sh("pkill -f 'aitun-clien[t].* -p 8101'", 20)
258
+ subprocess.Popen(["bash", "-c",
259
+ "nohup aitun -p 8101 > /tmp/k8b/aitun_api.log 2>&1 &"],
260
+ stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT,
261
+ stdin=subprocess.DEVNULL, start_new_session=True)
262
+ for _ in range(10):
263
+ time.sleep(3)
264
+ pub = tunnel_url_from("/tmp/k8b/aitun_api.log")
265
+ if pub:
266
+ break
267
+ print("\n[MODE] aitun 直连隧道 (-p 8101)")
268
+ print("[API] %s/v1" % (pub or "https://aitun.cc/<隧道码>"))
269
+ open(K8B + "/ONECLICK_DONE", "w").write("%.0fs" % (time.time() - t0))
270
+ P("==== ONECLICK_DONE %.0fs ====" % (time.time() - t0))
271
+ print("[OK] serving 上线 127.0.0.1:8101 key=1234 ctx 见 serve_oneclick.log")
272
+ print("[调用示例]")
273
+ print(" curl '%s/v1/chat/completions' \\" % (pub or "https://aitun.cc/<隧道码>"))
274
+ print(" -H 'Authorization: Bearer 1234' -H 'Content-Type: application/json' \\")
275
+ print(' -d \'{"model":"m13_r10_gsqrco_v1","messages":'
276
+ '[{"role":"user","content":"你好"}],"max_tokens":400}\'')
277
+ print("[openai python]")
278
+ print(" from openai import OpenAI")
279
+ print(" client = OpenAI(base_url='%s/v1', api_key='1234')"
280
+ % (pub or "https://aitun.cc/<隧道码>"))
281
+ print(" r = client.chat.completions.create(model='m13_r10_gsqrco_v1', "
282
+ "messages=[{'role':'user','content':'你好'}])")
283
+ print("[本地] http://127.0.0.1:8101/v1")
284
+ except Exception as e:
285
+ traceback.print_exc(file=log)
286
+ open(K8B + "/ONECLICK_FAIL", "w").write(repr(e)[:300])
287
+ P("==== ONECLICK_FAIL: %s ====" % repr(e)[:200])
288
+ print("FAIL:", repr(e)[:200], "— 日志:", LOG)
289
+ sys.exit(1)
290
+
291
+
292
+ if __name__ == "__main__":
293
+ main()