🚀 Fleet AHV — Sơ đồ & Chuẩn đặt tên

Cập nhật 11/07/2026. Nguồn chuẩn: /home/claudeproxy/Claude/ahv_fleet_runbook.md trên orchestrator. Giờ hệ thống toàn fleet = Asia/Ho_Chi_Minh (+7); riêng DB PostgreSQL giữ UTC nội bộ.

Chuẩn đặt tên (BẮT BUỘC)

  • Node = n<octet-cuối-IP> — 1:1 với IP, không bao giờ nhầm. Node mới .200 → n200.
  • Backend = {tính-năng}-{node} (external = ext-{provider}). Token tính năng: tryon, gen, swap, upscale, video, remove, rembg, scan, nsfw.
  • Category (API user gọi — KHÔNG đổi): try_on, ahv_image, faceswap, faceswap_video, upscale, remove_object, remove_background, scan_objects, nsfw_check + external.

Sơ đồ node hiện tại (5 node GPU nhà + pod RunPod + orchestrator)

NodeIPGPUBackend đang chạyGhi chú
n131103.253.21.13148G RTX PRO 5000tryon-131(6), gen-131(3), video-131(3), swap-131(30), upscale-131(10), swapprem-131(2)Workhorse; node duy nhất đủ VRAM cho premium video (pilot). video slots=3 khớp --workers 3 — xem mục 5
n81103.253.21.8124G RTX PRO 4000tryon-81(2), gen-81(1), swapprem-81(1)FLUX-ONLY (12/07: fsimage+fsvideo stopped — backend disabled thì service phải tắt; bật video-81 = start faceswap-video TRƯỚC). Là node DỰ PHÒNG video
n239103.253.21.23924G RTX PRO 4000tryon-239(2), gen-239(1), swapprem-239(1), video-239 TẮTNode nguồn rsync; fsimage stopped 12/07 (mồ côi 8.2G gây OOM tryon)
n183178.63.96.18320G (Hetzner)remove-9b-183(:2034), swap-183(20), upscale-183(8), stt-183(:2036), rembg/scan/nsfw-183(:2026)Node phụ trợ — KHÔNG chạy flux
n22161.248.81.2224G RTX PRO 4000tryon-22(2), swapprem-22(1)Vào trận 11/07 giữa sự cố quá tải; đủ 4 stack trên disk. Chạm trần công suất 145W lúc tải nặng — TỰ phục hồi
pod ada1213.173.108.25RTX 4000 Ada (RunPod)tryon-vn2-ada1(1)$0.28/h. Chạm trần 130W lúc tải nặng — TỰ phục hồi
pod scale1(đổi mỗi lần bung)RTX 4000 Ada (RunPod)tryon-vn2-scale1(1)Do autoscaler tự bung/tự trả (mục 9). Tắt lúc vắng là BÌNH THƯỜNG
pod music69.30.85.239RTX A5000 (RunPod)music_gen, music_cover, tts$0.27/h. ssh cổng 22198
Orchestrator15.235.200.66FastAPI :2022, PostgreSQL, JOB_WORKERS=80×4SPOF — cần dự phòng khi scale. Là chính máy đang chạy AHV AI — thao tác local, KHÔNG ssh vòng về

⚠️ Số trong ngoặc là slots. Bảng này rất dễ lạc hậu — nguồn sự thật là curl -s -u admin:Anhyeuem http://127.0.0.1:2022/admin/api/backends. Đối chiếu lại mỗi khi cân đội hình.

Nguyên tắc thiết kế: (1) mọi node đủ mọi stack trên DISK — cái gì "nóng" trên VRAM do vận hành quyết theo tải; (2) thêm tính năng = thêm 1 stack + 1 category backend, không sửa kiến trúc; (3) thêm node = chạy onboard script; (4) mọi chuyển dịch kiểu "bật mới song song → verify job thật → tắt cũ", không gián đoạn.

💎 Premium tier (premium: 0/1) — LIVE 12/07/2026

18/07 — Premium TRY_ON 1/0: premium:0 (mặc định) = 4 bước ~12s render; premium:1 = 8 bước ~23s (BE tính giá cao); khách gửi steps riêng thì steps thắng. Cùng khuôn faceswap premium. Retry chống "Queue full/timeout" phủ toàn bộ: try_on, gen, premium đơn, premium multi.

18/07 — remove_object nâng cấp: thêm mask_urls: [..] (≤10) — gộp mask xóa 1 lượt; kết hợp scan_objects (trả label/icon_url/mask_url từng vật) thành flow "bấm chọn vật để xóa". Mask có retention ~24h.

Thiết kế: API cũ giữ nguyên 100%. Client thêm field premium (int, mặc định 0) vào payload. premium:0/thiếu = bản thường; premium:1 = bản cao cấp. BE tính giá theo cờ này.

Faceswap ẢNH premium (ĐÃ LIVE)

POST /v1/jobs {"task":"faceswap","payload":{
  "target_url":"...",   # ảnh gốc (người)
  "source_url":"...",   # ảnh mặt
  "premium": 1          # 1 = head-swap 9B (đổi cả ĐẦU + TÓC); 0/thiếu = inswapper cũ
}}
  • Routing: orchestrator workers.py dispatch_task → category nội bộ faceswap_premium (swapprem-131/81/22 ON; swapprem-239 OFF — node chật). faceswap_multi LUÔN đi bản cũ.
  • Engine: flux service (:1994) endpoint POST /head_swap — FLUX.2-klein-9B + LoRA bfs_head (adapter thứ 2 cạnh tryon; thiếu file LoRA → node tự fallback, /head_swap trả 503, try_on KHÔNG ảnh hưởng). LoRA: /opt/flux-klein-tryon/lora/bfs/bfs_head_v1_flux-klein_9b_step3750_rank64.safetensors (đủ 4 node).
  • Số đo (stress 2 vòng 12/07): ~14s/ảnh model nóng; 21/21 premium succeeded; premium:0 regression nguyên vẹn; premium chen 1 slot semaphore flux → try_on chậm nhẹ khi premium dồn (chấp nhận, premium ít lượng).
  • Demo test: /public/webhook-demo.html#faceswap → ô "Chất lượng" chọn Premium.
  • Rollback tức thì: PUT enabled=false các swapprem-* (premium:1 sẽ lỗi "No available backends", premium:0 không ảnh hưởng).

Faceswap VIDEO premium (PILOT — chờ duyệt mở)

  • Công thức chốt: ComfyUI + Wan2.2 Bernini-R fp8 gốc (tải bằng HF_HUB_DISABLE_XET=1 — chiêu né xet stall VN) + BFS wan22 LoRA + Wan2.1 VAE 16ch (dùng nhầm wan2.2_vae 48ch = lỗi "48 vs 16") + node wrapper BerniniRV2VSimple; hybrid mode (HI CFG3.5 amplify + LO turbo CFG1).
  • Số đo model nóng: 3s clip = ~2-3 phút; 5s ≈ 5 phút; 10s ≈ 6-13 phút (thời gian ≈ tỉ lệ độ dài). VRAM 24-36G → cần node 48G. Trần độ giống mặt ~70-80% (giới hạn LoRA community — chờ V4). Script: run_bfs.py trên n131 (argv: guide head prefix frames steps mode strength quant).

Quy tắc VRAM & Xếp hình

  • flux (FLUX.2-klein-9B NF4, phục vụ cả /tryon/generate trên :1994) load-peak 14.7G, steady ~13G.
  • fs-image 8.8G; video 9.5G; diffusion remove ~3.5G; upscale nằm trong fs-image.
  • Node 24G: CHỈ flux + phụ ≤5G. flux + fs-image = OOM (thiếu 300MB — đã đo thật). Node 20G: không chạy flux. Chỉ n131 48G ôm được nhiều.
  • Thứ tự bật trên node ghép: flux load TRƯỚC (cần peak), dịch vụ khác SAU.
  • Mọi node flux PHẢI có drop-in PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True (chống VRAM phình → 503).
  • Video (inswapper) slot = free_VRAM/1.5G VÀ ≤3-4/node-24G (giới hạn phiên NVENC) — quá slot = lỗi "NVENC stream encode failed" / ONNX init OOM (đo thật 11-12/07).
  • Service bloat theo giờ chạy (flux >20G, fsvideo phình ~2-8h/lần khi tải nặng) → restart node đó lúc vắng khi free <2G.
  • Backend enabled ⇔ service running — backend tắt thì TẮT service (mồ côi ăn VRAM: n81 15.5G + n239 8.2G phát hiện 12/07); backend bật thì service PHẢI chạy (video-81 4 job fail 12/07).
  • KHÔNG sync config.py giữa các node (BASE_URL per-node — sự cố 12/07: 76 job link 404 trong 40 phút). Chỉ sync code (server.py/pipeline.py).
  • KHÔNG tải file lớn (model) trên node đang production — bão hòa băng thông làm HTTP/SSH treo (sự cố 11h15 12/07). Tải lúc drain/khuya hoặc --limit-rate.
  • GPU 100% util thì nâng slots là VÔ ÍCH (thrash) — chỉ có thêm node hoặc kịch bản ưu tiên.

Lệnh vận hành nhanh (từ orchestrator)

# Backend API (basic auth admin:***)
GET/POST/PUT http://127.0.0.1:2022/admin/api/backends   # POST=thêm, PUT=sửa {category,name,...}
# Bật tính năng X trên node N
systemctl start <unit> → ufw allow port → curl test → POST backend slots nhỏ → theo dõi job thật → tăng slots
# Tắt an toàn: PUT enabled=false TRƯỚC (drain) → systemctl stop
# Restart flux 1 node an toàn (flux :1994 phục vụ CẢ tryon + gen!):
#   disable cả tryon-X lẫn gen-X → drain ~20s → restart → health 200 → enable lại
  • KHÔNG restart orchestrator lúc tải cao (fail job đang chạy) — đổi env qua systemd drop-in, restart lúc khuya vắng.
  • SSH mọi node: key gpu_admin (password login đã TẮT trên n22, nên tắt dần các node cũ). Key tạm onboard phải xoá sau khi xong.
  • Admin panel Server: danh sách node ở /opt/ai-orchestrator-2022/collect_server_metrics.py — thêm node mới vào SERVERS.

⚡ Playbook sự cố (đã kiểm chứng thực chiến)

1. try_on quá tải (khách kêu chậm)

Ngưỡng vào kịch bản: p50>58s & p95>90s & running>90 building 2 nhịp — HOẶC khách kêu + p50>60s kéo dài. Lưu ý: p50/p95 vọt nhưng running thấp (<30) = burst tự tan (mẫu ~20h18 hằng đêm), KHÔNG can thiệp.

BẬC 1: PUT enabled=false gen-81 + gen-239   # dồn GPU n81/n239 cho try_on
BẬC 2: PUT enabled=false swap-131            # dồn n131 (faceswap vẫn chạy qua swap-183)
TAN (p50<40 & running<25): bật lại NGƯỢC bậc: swap-131 → gen-239 → gen-81

Thực chiến 10-11/07: p50 94s→29.5s trong ~90 phút (BẬC 1+2 + đưa n22 vào cấp tốc), 0 job mất.

2. 503 hàng loạt cùng 1 backend (≥3/nhịp)

= OOM node đó. Xem nvidia-smi; flux phình VRAM → restart flux node đó (quy trình drain ở trên); kiểm expandable_segments.

3. Link ảnh 404

Nguyên nhân kinh điển: (a) BASE_URL node trỏ sai IP (rsync đè config.py — bài học n22); (b) retention xoá sớm (đã chuẩn 24h mọi node). File thật luôn nằm trên node XỬ LÝ — cứu link đã phát bằng cách rsync files/ sang node có IP trong link.

4. disconnect/502 lẻ tẻ

1 lần đơn lẻ = transient, bỏ qua sau khi probe health OK. CÙNG backend ≥2/15 phút → probe, chết/chậm → restart service node đó.

5. Video quá tải / lỗi NVENC-ONNX

Sơ đồ chốt 28/07 — video KHÔNG ngồi chung node với flux try_on: chạy trên video-131 (48G, slots 3). video-81 là dự phòng (đang TẮT); video-239 và video-22 TẮT + systemctl disable. Lý do: flux try_on ăn ~14G, faceswap-video ăn ~7.5G — chung một card 24G chỉ còn ~2.6G thở → OOM hàng loạt lúc đỉnh (27/07: 543 job try_on chết vì OOM trên n22, dứt đúng phút n22 stop faceswap-video lúc 23:07).

⚠️ BẮT BUỘC LÀM NÓNG trước khi bật backend video: job đầu sau khi start service mất ~193s (source_detect nạp model 133s), job sau chỉ ~37s. Bật backend ngay = khách đầu tiên chờ 193s. Quy trình đúng: start service → health 200 → gửi 1 job thật để nạp model → mới PUT enabled=true → chờ nó xong 1 job thật từ hàng đợi → mới tắt node cũ (giữ luôn ≥2 backend video sống).

⚠️ /health 200 KHÔNG có nghĩa node còn sống: 28/07 video-239 có cặp ffmpeg treo 18.3 giờ, service --workers 1 nên bị khoá hoàn toàn — mọi job timeout thành 502 trong khi /health vẫn trả 200. Đã cài /usr/local/bin/ffmpeg_watchdog.sh (cron */5, giết ffmpeg >1200s) trên cả 4 node video 239/131/81/22. Nghi node video hỏng thì probe bằng job thật, đừng tin /health.

Node nào NVENC/ONNX >3/5phút → giảm slot node đó (free/1.5G) hoặc restart faceswap-video node đó (drain→restart→re-enable). Thực chiến 11/07: demand 75 → 3 node slot-theo-VRAM → 0 fail.

⚠️ slots PHẢI khớp số worker THẬT của service (bài học 28/07 22:50). video-131 khai slots=8 trong khi service chạy --workers 3 → job thứ 4–8 xếp hàng bên trong service, orchestrator không nhìn thấy, và thời gian chờ đó bị tính vào duration_sec. Hậu quả: có job 1 khung hình mất 341 giây mà dashboard không hề báo nghẽn. Sửa slots 8→3 (nóng, không cần restart) cho kết quả đo được:

số job chồng nhau   trước sửa      sau sửa
        1             63 s          38 s
        2             54 s          36 s
        3           76-79 s         31 s
        4          176-341 s        50 s  (đỉnh 80 s)  ← giảm hơn 4 lần
hàng đợi video sau khi sửa = 0 (không đánh đổi bằng việc bắt khách chờ)

Dấu hiệu nhận biết KHÔNG phải là số worker — uvicorn 1 worker vẫn chạy song song qua threadpool, nên đừng suy ra "1 worker thì slots phải = 1" (try_on trên node 131 khai slots=6, service 1 worker, vẫn 8 giây đều). Dấu hiệu thật là tương quan chồng-lấn ↔ độ trễ. Cách đo (thay <TASK>):

with v as (select started_at, finished_at, duration_sec from jobs
           where task='<TASK>' and status='succeeded' and finished_at > now() - interval '60 min'),
     o as (select a.duration_sec,
                  (select count(*) from v b
                   where b.started_at < a.finished_at and b.finished_at > a.started_at) k
           from v a)
select least(k,8) chong_lan, count(*) n, round(avg(duration_sec)::numeric,1) tb,
       round(max(duration_sec)::numeric,1) mx
from o group by 1 order by 1;

Đã soát toàn hệ thống 28/07 23:05: faceswap / upscale / remove_object phẳng theo chồng lấn (slots cao vẫn an toàn vì job chỉ 2–3 giây); ahv_image nhích nhẹ nhưng mẫu quá ít để kết luận. Chỉ video bị lỗi này.

🐞 CHƯA SỬA — job video ra 1 khung hình nhưng báo succeeded (chẩn đoán 29/07 02:00). Tần suất 3 lần/24h trên 1028 job (0,3%). Khách đợi ~5,5 phút rồi nhận video 1 khung, không tiếng, mà hệ thống báo thành công nên không ai biết. Chuỗi log trên video-131:

Decoder frame read timeout (60s), killing ffmpeg
WATCHDOG: killing stuck ffmpeg pid=... age=327s
CUVID decode failed, falling back to stream decode
Stream decode: 1 frames
NVENC failed (rc=-9, write_ok=False)        ← rc=-9 = bị watchdog SIGKILL
libx264 recovery encode: 1 frames in 328.15s
DONE: 1 frames                              ← ghi succeeded

Nguyên nhân: đường giải mã có timeout 60s (server.py dòng 604) chạy đúng; đường mã hóa NVENC không có timeout nào → chỉ có watchdog nền FSV_FFMPEG_MAX_AGE=300 (dòng 286) cứu, nên mất ~5,5 phút.

⚠️ BẪY: ĐỪNG hạ FSV_FFMPEG_MAX_AGE xuống ~90s (sửa được bằng biến môi trường nên rất dễ làm). Video dài HỢP LỆ có job 1265 giây (21 phút) và nó giữ một tiến trình ffmpeg suốt thời gian đó → hạ ngưỡng tuổi là giết oan video dài. Sửa đúng cần code: (1) timeout cho thao tác ghi vào bộ mã hóa, tính theo tiến độ chứ không theo tuổi tiến trình; (2) khi đường cứu hộ chỉ vớt được 1 khung thì trả lỗi, đừng trả succeeded. Cách tìm lại ca lỗi:

select to_char(finished_at + interval '7 hours','DD/MM HH24:MI') xong,
       round(duration_sec::numeric,1) giay, result->'meta'->>'frames' khung
from jobs where task='faceswap_video' and status='succeeded'
  and (result->'meta'->>'frames')::int <= 2
  and finished_at > now() - interval '24 hours' order by finished_at desc;

6. Job kẹt / metric running phồng ở đỉnh

Watchdog 900s chạy trễ dưới tải (coroutine đói — mục Audit 6/7). Job không leak vĩnh viễn (recover lúc boot). Fix triệt để cần sửa server.py + restart orchestrator lúc vắng (đang chờ duyệt).

7. Node thành "hố đen" — queue flux đầy request ma (17/07)

Dấu hiệu: health node queue_size cao bất thường (≥10) so với slots, mọi job qua node đó siêu chậm, các node khác rảnh. Nguyên nhân: request timeout phía orchestrator nhưng flux vẫn giữ chỗ trong queue.

1. PUT enabled=false các backend của node (tryon-X, swapprem-X, gen-X...)
2. Chờ 45-60s cho job thật đang render xong
3. ssh node: systemctl restart flux-klein-tryon
4. Chờ health model_loaded:true → PUT enabled=true gắn lại TẤT CẢ backend vừa tắt
LƯU Ý: làm XONG TRỌN node này rồi mới sang node khác — không để category nào 0 backend dù 1 phút.

8. Restart orchestrator — quy tắc sống còn (bài học 85 job 17/07, cập nhật 28/07)

Từ 27/07 restart KHÔNG còn giết job khách nữa. recover_stale_jobs() lúc boot: job đang running và tạo trong vòng 30 phút → trả về queued + đẩy lại hàng đợi; job cũ hơn/mồ côi → failed. Dùng advisory lock pg_try_advisory_lock(918273645) nên trong 4 worker chỉ 1 worker chạy hồi phục — không bao giờ re-queue trùng. Đã verify 4 lần (11/15/7/3 job in-flight, không mất job nào).

# Đếm in-flight trước khi restart (chỉ để biết bao nhiêu job sẽ bị re-queue):
SELECT status, count(*) FROM jobs WHERE status IN ('queued','running') GROUP BY status;
# LƯU Ý: chỉ có 'queued' và 'running'. KHÔNG có status 'processing' — đừng lọc theo nó.

Vẫn nên tránh restart lúc tải cao: job bị re-queue phải chạy lại từ đầu (khách đợi thêm 1 lượt) và cả 4 worker cùng lúc mất kết nối đang mở. Cần đổi env → dùng systemd drop-in, restart lúc vắng.

Onboard node GPU mới

bash /home/claudeproxy/Claude/onboard_node.sh <IP>   # (node đã nhận key gpu_admin)

Script tự làm: rsync 4 stack từ n239, copy systemd units, libnvinfer, set BASE_URL/FSV_PUBLIC_BASE=IP mới, expandable_segments, ufw. SAU KHI rsync ALL_DONE — 2 việc BẮT BUỘC (bài học n22 10-11/07):

  1. Chạy lại bước set BASE_URL — rsync ĐÈ config.py bằng bản node nguồn → link sẽ trỏ sai IP.
  2. Kiểm Python: venv build cho python3.10; node OS mới (3.12) phải apt install python3.10 (deadsnakes) + ln -sf /usr/bin/python3.10 venv/bin/python3.

Rồi: start flux → health 200 → POST backend tryon-<octet> slots 2 → theo dõi job thật (link đúng IP!) → xoá key tạm onboard_tmp → thêm vào SERVERS admin panel → tắt password SSH.

Giám sát & Ngưỡng

Kiến trúc 3 tầng (từ 16/07 — vận hành THEO NGƯỠNG, không theo giờ): (1) scripts/auto_bac.sh crontab root */2ph — vượt ngưỡng (stuck>150s hoặc ≥8 job >120s/10ph) tự BẬC1+BẬC2, yên 25ph tự trả FULL (log /var/log/auto_bac.log); (2) scripts/alert_metrics.sh — nguồn số cho chuông Monitor của phiên AI (75s/nhịp) réo AI vào xử + nhắn anh kết quả; (3) cron trực AI 12ph = backup + leo thang (cách ly node, role-swap). Im lặng khi xanh; chỉ nhắn khi sự cố/đã-xử/cần-quyết. Đếm in-flight PHẢI gồm status running.

⚠️ GIỚI HẠN của auto_bac.sh — ĐỪNG mất công vá (đã kiểm chứng 28/07): nó chỉ thấy job chậm (STUCK đếm queued/running, DMG đếm job succeeded >120s). Job chết vì OOM fail tức thì nên vô hình với cả hai. Nhưng thêm điều kiện OOM vào trigger cũng VÔ ÍCH: log 27/07 cho thấy valve đã nổ 41 lần liên tục 18:00–23:00 mà 543 job vẫn chết OOM — van đã mở hết cỡ sẵn. Lý do gốc: BẬC1/BẬC2 chỉ tắt được gen/swap, không giải phóng nổi VRAM mà service faceswap-video đang giữ. Cách duy nhất chữa OOM là gỡ service video khỏi node chạy flux try_on (xem mục 5).

  • Khung tải VN (+7): sáng 7-9h, trưa 11-14h, tối 19-22h nặng nhất (+burst ~20h18 tự tan), volume đang TĂNG từng ngày kể cả khuya.
  • Lỗi INPUT — KHÔNG tính sự cố hệ thống: no-face, mask trống, 400/404, NSFW_BLOCKED, "Failed to download image" (storage 5.223.78.252 xoá input ngay — lỗi try_on nhiều nhất), "image file is truncated".
  • Đọc node lỗi bằng field backend trong error JSON (tên chuẩn) — ĐỪNG tin text "VN2 try-on" (label cũ hardcode).
  • DB timestamp = UTC (giờ máy = VN+7). Node xử lý ≠ node lưu file.

9. Pod RunPod mồ côi & autoscaler thất bại trong im lặng (29/07)

⚠️ Mỗi ca trực phải ĐỐI CHIẾU danh sách pod RunPod với danh sách backend. Pod không nằm trong backend nào = đốt tiền không công. Đêm 29/07 bắt được pod ahv-scale-1 chạy khỏe, qua canary gate, nhưng không có trong cấu hình — 1,8 giờ không nhận một job nào.

RP=$(grep -oP '(?<=RUNPOD_API_KEY=).*' /home/claudeproxy/Claude/.secrets/runpod.env)
curl -s -X POST "https://api.runpod.io/graphql?api_key=$RP" -H "Content-Type: application/json" \
  -d '{"query":"query { myself { pods { id name desiredStatus costPerHr runtime { uptimeInSeconds ports { ip publicPort } } } clientBalance } }"}'
# rồi so với: curl -s -u admin:Anhyeuem http://127.0.0.1:2022/admin/api/backends

Hai lỗi chồng nhau trong rp_autoscale.sh (đã vá 29/07, backup .bak-regfix-29-07):

  • Nguyên nhân — $POD rỗng mang hai nghĩa: thật sự không có pod nào, HOẶC gọi API RunPod thất bại/quá hạn 20s. Script cũ không phân biệt được → một lần API chập chờn là tưởng đội hình trống và dựng pod TRÙNG. Đã sửa: đọc phản hồi thô trước, không thấy khóa "pods" thì bỏ qua nhịp thay vì đoán bừa.
  • Hậu quả — bước đăng ký dùng POST (chỉ tạo mới) trong khi đường SCALE-DOWN cố ý giữ lại entry (chỉ enabled=false). Nên từ lần scale-up thứ hai trở đi POST luôn bị từ chối "already exists", script bỏ qua lỗi và vẫn ghi SCALE-UP DONEmọi lần tự tăng công suất đều thất bại trong im lặng. Nguy hiểm hơn tiền: lúc có sóng tải, hệ thống tưởng đã có thêm máy mà thực ra không có gì. Đã sửa: POST → nếu trùng thì PUTkiểm chứng thật sự có trong danh sách backend → nếu vẫn không vào thì tự podTerminate.

Bài học chung: đừng tin mã trả về, hãy kiểm chứng trạng thái cuối. Log ghi "DONE" không có nghĩa là xong. Ngưỡng hiện tại: scale-up khi C>190PREV>190 VÀ không có pod ahv-scale-*; scale-down khi C<120PREV<120. Log: /var/log/rp_autoscale.log.

Đặt tên pod: pod bung TAY phải đặt tên khác ahv-scale-* (ví dụ ahv-extra-1), nếu không autoscaler sẽ terminate nhầm.

10. Service systemd hỏng âm thầm — kiểm NRestarts (29/07)

ai-orchestrator.service (cổng 8080) trỏ ExecStart tới /usr/bin/uvicorn không tồn tại → status=203/EXEC, systemd thử lại mãi: NRestarts=735657. Nó chưa từng chạy được lần nào nhưng vẫn enabled, và gần như chắc chắn là nguồn làm tràn journal. Service thật đang phục vụ là ai-orchestrator-2022 (NRestarts=0). Đã disable --now.

# soát định kỳ toàn máy — unit nào restart bất thường
systemctl list-units --type=service --state=activating --no-pager
for U in $(systemctl list-unit-files --type=service --state=enabled --no-pager | awk '{print $1}' | grep '\.service$'); do
  N=$(systemctl show "$U" -p NRestarts --value 2>/dev/null); [ "${N:-0}" -gt 100 ] && echo "$U NRestarts=$N";
done

📚 Bài học sự cố (đọc trước khi vận hành)

NgàySự cốBài học
04-05/07remove_object LaMa → diffusionNâng cấp kiểu song song + fallback; đo A/B 10 case trước khi chốt
06/07Link 404 hàng loạt2 lỗi chồng nhau: retention 1h quá ngắn (→24h) + BASE_URL .81 sai. Fix cả gốc lẫn onboard script
06/07Audit 2 vòngBug latent (FSV_PUBLIC_BASE .183, flux .183 còn enabled) phải quét chủ động, đừng chờ nổ
10-11/07try_on quá tải kỷ lục (p50 108s, p95 912s)3 node bão hòa thật sự → BẬC 1+2 + n22 cấp tốc. 3 bug khi onboard gấp: rsync đè BASE_URL, python 3.12 vs venv 3.10, log DONE giả do kill vòng lặp (luôn dùng exit-code marker)
11/07Video SPOF: drain n131 test → 305 job video "no backend"Video giờ ≥2 node (video-131+video-239); TRƯỚC khi rút node khỏi category 1-node phải bật dự phòng; test GPU ban ngày chỉ khi có node gánh
11-12/07Video đỉnh trưa 70+ concurrent, NVENC/ONNX failSlot video = free/1.5G và ≤3-4/node (NVENC session limit); fsvideo bloat theo giờ → restart lúc vắng
12/07Premium ảnh deploy — 3 bug tự bắt trong audit(a) KHÔNG sync config.py giữa node (BASE_URL — 76 job 404/40min); (b) service mồ côi ăn VRAM 2 node (n81 15.5G, n239 8.2G → OOM tryon-239); (c) backend enabled ⇔ service running. Tải model lớn trên node production làm nghẽn băng thông (HTTP/SSH treo ~10min)
09-10/07Đánh giá FLUX 4B vs 9BNF4 trên card này: 4B KHÔNG nhanh hơn 9B (nghẽn băng thông), chất lượng thấp hơn → gen/try_on giữ 9B; 4B chỉ đáng cho head_swap (VRAM 4.2G); giảm bước 8→4 mới là đòn bẩy tốc độ thật (distilled)
13/07SLA thật của kháchJob xong sau ~120s = khách ĐÃ BỎ (anh chốt chuẩn chờ tối đa 120s, target p50 ~25s). Metric thiệt hại = số job >120s + số job fail, KHÔNG phải "0 lỗi hệ thống". 13/07: 26% job try_on >120s dù "thành công"
14-15/07Chuẩn hoá driver fleet + n81 crashCả 4 node flux lên driver 610.43.02 + expandable_segments → 0 OOM từ đó. Node vừa reboot cần warm 30-60 phút (avg/job cao giả) — đo per-node trước khi kết luận node hỏng. Purge sạch driver cũ (metapackage remove để sót 16 pkg conflict)
15/07Trùng task 5 lần + cron session chết lặng lẽChuỗi ScheduleWakeup tự nối đẻ orphan → cấm. Cron session-only CHẾT khi phiên reset không báo → mọi phòng thủ sống còn phải nằm TRÊN SERVER (system crontab), phiên AI chỉ là tầng leo thang
16/07Sóng trưa lọt lưới (185 job >120s)Hạ bậc phản ứng theo nhịp trực = quá chậm với sóng dồn cụm → watchdog auto_bac.sh */2ph trên orchestrator: stuck>150s hoặc ≥8 job >120s/10ph → tự BẬC1+2, yên 25ph tự trả full. Anh chốt: vận hành THEO NGƯỠNG, không auto theo khung giờ
17/07Sóng kỷ lục 15-24 job/ph ×100ph (601 fail + 1.4k >120s)Chuỗi khuếch đại: chậm → timeout → orchestrator gửi lại → queue flux đầy 20 → 503 "Queue full" → job khách CHẾT. Fix: workers.py retry ≤6×20s re-pick node khi Queue full (đã deploy). Queue flux tồn request "ma" (client bỏ nhưng flux giữ chỗ) → node thành hố đen: cách ly → restart flux → gắn lại
17/072 bài học đo lường + 1 tai nạn restart(a) In-flight = status running (queued/processing chỉ có khi backlog) — đếm thiếu running làm check "idle" mù → restart orchestrator giết ~85 job đang chạy. (b) p50 bucket hiện tại bị survivorship bias (job chậm chưa xong chưa được đếm) — chỉ kết luận sóng rút khi running thấp VÀ arrival giảm. (c) Slots lệch (n131=6) làm 1 node ôm 14 job xếp hàng trong khi node khác rảnh → mọi node try_on slots=2
18/07Premium try_on 1/0 + rollout sót nodetry_on premium:0=4 bước (~12s render), premium:1=8 bước (~23s), steps tường minh được tôn trọng (workers.py dòng ~895). Rollout cuốn chiếu bị đứt phiên giữa chừng ĐÃ BỎ SÓT n131 còn FORCE_TRYON_STEPS=4 → khách premium nhận hàng 4 bước — audit stress mới lộ. Quy tắc: sau mọi rollout nhiều node PHẢI quét lại drop-in TẤT CẢ node
18/07Stress 16-job lộ lỗ hổng retry premiumĐường head_swap premium (đơn + multi) chưa có đợi-thử-lại → 2 job chết "Queue full" tức thì trong stress. Fix: _call_prem_retry (4 lần×12s re-pick). BÀI HỌC: mọi đường gọi flux ĐỀU phải qua retry — kiểm bằng grep _call_backend không được gọi trực tiếp cho category chạy flux
18/07Service mồ côi đợt 2flux-kontext (n239 port 1993, 0 backend dùng — bom OOM nếu bị gọi) + faceswap-restore (n81, boot-enabled) → disable cả 2. Quy tắc audit định kỳ: systemctl running vs backends enabled phải khớp 1-1
18/07Xóa nhiều vật thể + hệ ảnh 9Bremove_object nhận mask_urls[] (gộp OR ≤10 mask); flow scan→tick→xóa hoàn chỉnh. /generate của flux = img2img theo prompt → pilot khôi phục ảnh cũ + làm đẹp + đổi tóc (anh chốt dùng 9B cho restore). Deploy orchestrator = deploy_wait.sh nohup chờ 0-inflight (gồm status running)

Việc treo chờ quyết: NODE MỚI (cấp thiết — 3 ngày liên tiếp sóng vượt trần 4 node ~12 job/ph), mở bán premium video, retention bảng tasks, label "VN2" hardcode, cache input chống download-fail, orchestrator dự phòng (SPOF), retry queue-full cho gen/ahv_image (mới làm cho try_on). ĐÃ XONG 13-17/07: premium multi 2 người (live), music_gen v1 (code giữ, backend tắt theo lệnh anh), driver fleet 610, watchdog theo ngưỡng, retry queue-full try_on.

AHV AI Orchestrator v2.0

Hệ thống điều phối AI đa tác vụ – quản lý hàng đợi, phân phối backend GPU, webhook callback, và giám sát thời gian thực.

Stack

Python 3.10+ · FastAPI · Uvicorn (uvloop + httptools) · PostgreSQL (psycopg async) · Systemd

Architecture

Client ──► Nginx (SSL/Cloudflare) ──► FastAPI (4 workers, uvloop) │ ├─► asyncio.Queue ──► 40 Job Workers ──► GPU Backends │ ├─► PostgreSQL (jobs, ema_predictions, webhook_logs) │ ├─► SSE Stream ──► Dashboard (realtime) │ └─► Webhook (3 retries, exponential backoff)

Job Processing Flow

  1. Client gửi POST /v1/jobs với task, payload, webhook_url
  2. Job lưu DB với status queued, trả về job_id + predict_time (EMA)
  3. Webhook phase 1: gửi event queued
  4. Worker lấy job, chọn backend GPU (round-robin + semaphore)
  5. Webhook phase 2: gửi event started
  6. Backend xử lý xong → trả kết quả
  7. Webhook phase 3: gửi event finished với URL + meta
  8. EMA predictor tự học từ duration thực tế

Installation & Setup

Requirements

  • Python 3.10+
  • PostgreSQL 14+
  • Nginx (reverse proxy, SSL)
  • Linux with Systemd

Quick Start

cd /opt/ai-orchestrator-2022
pip install -r requirements.txt

# Create database
sudo -u postgres createdb ahv_orchestrator
sudo -u postgres psql -c "CREATE USER ahv_user WITH PASSWORD 'ahv_pass_2026';"
sudo -u postgres psql -c "GRANT ALL ON DATABASE ahv_orchestrator TO ahv_user;"

# Run (development)
uvicorn server:app --host 0.0.0.0 --port 2022 --reload

# Run (production)
sudo systemctl start ai-orchestrator-2022
sudo systemctl enable ai-orchestrator-2022

Service Management

sudo systemctl status ai-orchestrator-2022   # View status
sudo systemctl restart ai-orchestrator-2022  # Restart (~2s) — job in-flight được re-queue, xem mục 8
sudo systemctl reload ai-orchestrator-2022   # SIGHUP → uvicorn thay 4 worker kiểu CUỐN (restart_all)
                                             # Cổng 2022 do tiến trình cha giữ nên KHÔNG đóng (không refuse
                                             # kết nối), NHƯNG request đang chạy trên worker bị giết vẫn đứt.
                                             # => "không rơi kết nối", KHÔNG phải "không gián đoạn job".
journalctl -u ai-orchestrator-2022 -f        # Live logs

Environment Variables

Database

VariableDefaultDescription
DB_HOSTlocalhostPostgreSQL host
DB_PORT5432PostgreSQL port
DB_NAMEahv_orchestratorDatabase name
DB_USERahv_userDatabase user
DB_PASSWORDahv_pass_2026Database password

API & Auth

VariableDefaultDescription
API_KEYanhyeuemAPI key for clients
ADMIN_PASSAnhyeuemAdmin panel password (Basic Auth)

Timeout & Performance

VariableDefaultDescription
REQUEST_TIMEOUT300Backend call timeout (seconds)
CONNECT_TIMEOUT15Connection timeout (seconds)
JOB_MAX_RUNTIME900Max job runtime before watchdog kills it
MAX_QUEUE_SIZE1000Maximum queue capacity
JOB_WORKERS40Concurrent worker count

Webhook

VariableDefaultDescription
WEBHOOK_TIMEOUT12Webhook delivery timeout (seconds)
WEBHOOK_RETRIES3Max retry attempts
WEBHOOK_BACKOFF_BASE0.6Exponential backoff base (seconds)
WEBHOOK_SECRET""HMAC-SHA256 signing secret

EMA Predictor

VariableDefaultDescription
PREDICT_EMA_ALPHA0.25Learning rate (0–1, higher = more responsive)
PREDICT_MIN_SEC3Minimum predicted time (seconds)

Database: Table jobs

Main table storing all job records. Each API call creates one row.

ColumnTypeDescription
job_idTEXT PKUUID unique identifier
taskTEXT indexedTask type: faceswap, try_on, ahv_image, ...
payloadJSONClient-provided parameters
webhook_urlTEXTCallback URL
statusTEXT indexedqueued · running · succeeded · failed · rejected
progressINTEGER0–100
resultJSONSuccess result: {url, filename, meta}
errorJSONError details: {code, message}
created_atTIMESTAMPJob creation time
started_atTIMESTAMPProcessing start time
finished_atTIMESTAMPCompletion time
duration_secFLOATActual processing duration (seconds)
predict_timeINTEGERPredicted duration from EMA (seconds)
eta_utcTIMESTAMPEstimated completion time
attemptsINTEGERRetry attempt count
expires_atTIMESTAMPAuto-delete after this time

Indexes: idx_jobs_created, idx_jobs_expires, idx_jobs_status_created

Database: Table ema_predictions

Stores Exponential Moving Average values for time prediction per task variant.

ColumnTypeDescription
keyTEXT PKTask variant key (e.g. faceswap:up1:ms1080)
valueFLOATCurrent EMA estimate (seconds)
countINTEGERNumber of samples learned
updated_atTIMESTAMPLast update time

EMA Formula: new = alpha × observed + (1 - alpha) × old. Alpha = 0.25 means 25% weight on new observations.

Database: Table webhook_logs

Tracks failed webhook deliveries for debugging. Auto-cleaned after 7 days.

ColumnTypeDescription
idSERIAL PKAuto-increment ID
job_idTEXT indexedRelated job
eventTEXTqueued · started · finished
urlTEXTWebhook URL that failed
attemptINTEGERWhich retry attempt
status_codeINTEGERHTTP status code returned
errorTEXTError message
created_atTIMESTAMPLog timestamp

API: Jobs

POST /v1/jobs — Create Job

Creates a new async processing job.

curl -X POST http://15.235.200.66:2022/v1/jobs \
  -H "Content-Type: application/json" \
  -H "X-API-Key: anhyeuem" \
  -d '{
    "task": "faceswap",
    "payload": {
      "target_url": "https://example.com/photo.jpg",
      "source_url": "https://example.com/face.jpg"
    },
    "webhook_url": "https://your-server.com/webhook"
  }'

Response

{
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "queued",
  "predict_time": 10,
  "eta_utc": "2026-06-14T10:00:10Z"
}

GET /v1/jobs/{job_id} — Get Job Status

Success Response

{
  "job_id": "a1b2c3d4-...",
  "task": "faceswap",
  "status": "succeeded",
  "progress": 100,
  "result": {
    "url": "http://15.235.200.66:2022/uploads/result.jpg",
    "filename": "result.jpg",
    "meta": {"width": 1080, "height": 1920, "backend": "FSVN1"}
  },
  "duration_sec": 3.5
}

Error Response

{
  "job_id": "a1b2c3d4-...",
  "status": "failed",
  "error": {"code": 502, "message": "Backend timeout after 300s"}
}

API: Upload & NSFW

POST /upload — Upload File

curl -X POST http://15.235.200.66:2022/upload -F "file=@photo.jpg"
{"url": "http://15.235.200.66:2022/uploads/abc123.jpg", "filename": "abc123.jpg"}

POST /v1/nsfw-check — NSFW Check

curl -X POST http://15.235.200.66:2022/v1/nsfw-check \
  -H "Content-Type: application/json" \
  -d '{"image_url": "https://example.com/photo.jpg", "strict": "medium"}'
{"status": "ok", "nsfw_score": 0.02, "is_nsfw": false, "threshold": 0.5, "label": "safe"}

API: Health & Diagnostics

EndpointDescription
GET /healthz{"ok": true, "status": "healthy", "queue_size": 5}
GET /diag/backendsAll backend nodes: slots, in-flight, enabled, last_error
GET /diag/queue{"queued": 10, "running": 5}
GET /statsRecent jobs list (with ?limit= and ?task= filter)
GET /daily-statsDaily aggregation (timezone-aware via ?tz=)
GET /eventsSSE stream for realtime dashboard updates

API: Admin Statistics

Auth Required: All admin endpoints require Basic Auth (username: admin, password: ADMIN_PASS)

EndpointDescription
GET /admin/stats/overviewSummary: total jobs, success rate, avg duration, queue status
GET /admin/stats/hourlyHourly traffic breakdown + avg response time
GET /admin/stats/by-kindJob distribution by task type
GET /admin/stats/daily-trend7-day trend (stacked area chart data)
GET /admin/stats/top-usersTop API users by webhook URL
GET /admin/stats/top-errorsMost common error messages
GET /admin/stats/backend-perfPer-backend performance metrics
GET /admin/stats/exportExport jobs data (?fmt=json or csv)
GET /admin/stats/liveLive counters: queued, running, succeeded, failed
GET /admin/stats/webhook-failuresFailed webhook delivery logs
DELETE /admin/stats/webhook-failuresPurge all webhook failure logs

Query params: ?tz=Asia/Ho_Chi_Minh, ?days=7, ?limit=20

API: Backend Management

EndpointMethodBody
/admin/api/backendsGET
/admin/api/backendsPOST{"category", "name", "url", "slots", "enabled"}
/admin/api/backendsPUT{"category", "name", "url", "slots", "enabled", "new_name"}
/admin/api/backendsDELETE{"category", "name"}
/admin/api/retryGET / POSTGet/set retry counts per task type
/admin/api/tryon-settingsGET / POSTVirtual Try-On default settings
/admin/api/cx-settingsGET / POSTCX Image model settings

Tasks: Faceswap & Upscale

faceswap

Swap a face onto a target image. Timeout: 180s

Required: target_url, source_url

faceswap_multi

Swap multiple faces. Timeout: 180s

Required: target_url, source_urls[]

faceswap_video

Swap face in video, preserves audio. Timeout: 600s

Required: target_url, source_url

upscale

AI image enhancement + upscaling. Timeout: 300s

Required: target_url or image_url

Optional Parameters

ParamDefaultDescription
max_side1080Max output dimension (px)
max_faces0 (all)Limit number of faces to swap
upscale1Enable upscaling (0/1)
restore_face1Enable face restoration (0/1)
sharpen0.12Sharpening strength (0–1)

NSFW Protection: All face tasks automatically run NSFW check. Violations result in status: "rejected" with code NSFW_BLOCKED.

Tasks: Image Generation

ahv_image

Smart-routed text-to-image / image-to-image. Timeout: 300s

Backends: VN2 GPU, CX API, FlowProxy (auto-selected)

gg_image

Google Imagen-4 via FlowProxy. Timeout: 300s

Async polling: submit → poll → download

cx_image

CX/GPT Image API. Timeout: 120s

Direct API call with auto-upload

minimax_image

MiniMax image generation. Timeout: 120s

Supports I2I with image_url

Payload

ParamRequiredDescription
promptYesText description of desired image
image_urlNoSource image for image-to-image
width / heightNoOutput dimensions
aspect_ratioNo1:1, 16:9, 9:16
modelNoModel override (backend-specific)
stepsNoInference steps
guidanceNoGuidance scale
seedNoRandom seed for reproducibility

ahv_image routing: VN2 + no image_url → retries non-VN2. VN2 + image_url → VN2 GPU I2I. CX backend → CX API. FlowProxy → Imagen-4.

Tasks: Video Generation

ahv_video

Image-to-video via Google Veo / FlowProxy. Timeout: 600s

Required: prompt

minimax_video

MiniMax text/image-to-video. Timeout: 600s

Required: prompt

Payload

ParamRequiredDescription
promptYesVideo description
image_urlNoSource image (for I2V)
durationNoVideo duration (default: 6s)
aspect_ratioNo16:9, 9:16, 1:1
fpsNoFrames per second

Tasks: Virtual Try-On

try_on

Virtual clothing try-on. Timeout: 300s

Smart routing: VN2 (sync) / PiAPI (async) / CX / FlowProxy

Payload

ParamRequiredDescription
person_image_urlYesPhoto of the person (aliases: model_input, person_image)
garment_image_urlYesPhoto of garment (aliases: dress_input, garment_image)
top_urlNoUpper garment (for split top/bottom)
bottom_urlNoLower garment (for split top/bottom)
promptNoAdditional instructions

Smart Routing:
VN2 (port 1994) → Direct sync call (300s timeout)
PiAPI/Kling → Async submit + polling
CX → Multi-image prompt approach
FlowProxy → Imagen-4 with try-on prompt

Tasks: Image Tools

remove_object

Remove objects from image. Timeout: 120s

image_url, prompt (describe object)

scan_objects

Detect & label objects. Timeout: 60s

image_url

remove_background

Auto background removal. Timeout: 60s

image_url

nsfw_check

Content moderation check. Timeout: 30s

image_url, strict (low/medium/high)

Backend System

Multi-backend Architecture

Each task type has a category with multiple nodes (GPU servers):

Category: faceswap
├── FSVN1   (30 slots) → http://103.253.21.239:1993/faceswap
└── FSVN48G (30 slots) → http://178.63.96.183:1993/faceswap

Category: try_on
├── TryOn-VN2 (3 slots) → http://103.253.21.239:1994/tryon
└── TryOn-HZ1 (3 slots) → http://178.63.96.183:1994/tryon

Selection Algorithm

  1. Iterate enabled backends in order
  2. Pick first backend with free semaphore slot
  3. If all busy → pick least-loaded backend
  4. Disabled backends → skipped entirely

Backend Class Properties

PropertyTypeDescription
namestrDisplay name (e.g. "FSVN1")
urlstrGPU endpoint URL
slotsintMax concurrent requests
enabledboolActive/inactive toggle
api_keystrPer-node API key (MiniMax, CX, ...)
semSemaphoreRuntime concurrency limiter
total_doneintJobs processed (live counter)
last_errorstrMost recent error (diagnostics)

Hot Reload: Add/edit/delete backends via Admin UI or API — no service restart needed. Config persists in backends_config.json.

Webhook System

3-Phase Event Flow

Job Created ──► {"event": "queued", "status": "queued", "predict_time": 10} Job Running ──► {"event": "started", "status": "running", "progress": 0} Job Done ──► {"event": "finished", "status": "succeeded", "url": "...", "meta": {...}}

Success Payload

{
  "job_id": "a1b2c3d4-...",
  "task": "faceswap",
  "status": "succeeded",
  "event": "finished",
  "progress": 100,
  "url": "http://15.235.200.66:2022/uploads/result.jpg",
  "filename": "result.jpg",
  "meta": {"backend": "FSVN1", "width": 1080, "height": 1920, "duration_sec": 3.5}
}

Error Payload

{
  "job_id": "a1b2c3d4-...",
  "status": "failed",
  "event": "finished",
  "error": {"code": 502, "message": "Backend timeout after 300s"}
}

NSFW Rejected Payload

{
  "job_id": "a1b2c3d4-...",
  "status": "rejected",
  "event": "finished",
  "error": {"code": "NSFW_BLOCKED", "message": "Content violates policy"}
}

Retry & Security

FeatureDetail
Retry count3 attempts
BackoffExponential: 0.6s → 1.2s → 2.4s (+ random jitter)
SignatureX-Signature: sha256=<hex> (HMAC-SHA256, if WEBHOOK_SECRET set)
Failure loggingSaved to webhook_logs table, viewable in Admin UI
Auto cleanupLogs older than 7 days automatically deleted

Auth & Security

API Key Authentication

Path PatternAuth Required
/, /public/*, /upload, /healthzNo
/stats, /events, /tasksNo
/v1/jobs, /v1/nsfw-checkAPI Key (X-API-Key header)
/admin/api/*Basic Auth (admin)

CORS

All origins allowed (*) with credentials support.

NSFW Protection

Auto-applied to: faceswap, faceswap_video, upscale, try_on.

Violation → status: "rejected", code: NSFW_BLOCKED

Web Interfaces

Landing Page

Product overview, features, pricing, code examples

Dashboard

Realtime monitoring via SSE, traffic charts, job list

Admin Panel

Backend management, retry config, stats, webhook logs

API Demo

Test all APIs directly in browser with live preview

API Docs

Complete API reference with examples & error codes

Wiki

This page — full system documentation

File Structure

/opt/ai-orchestrator-2022/
├── server.py              # FastAPI app: routes, DB, queue, webhook, auth, SSE (~1800 lines)
├── workers.py             # Task dispatch: backend calls, normalize responses (~1370 lines)
├── handlers.py            # Helpers: NSFW, webhook retry, EMA, payload builders (~620 lines)
├── backends.py            # Backend registry: config, node management, selection (~430 lines)
├── backends_config.json   # Backend node config (persists across restarts)
├── .env                   # Local environment overrides
├── requirements.txt       # Python dependencies
├── README.md              # Project documentation
│
├── public/
│   ├── index.html         # Landing page
│   ├── dashboard.html     # Realtime monitoring dashboard
│   ├── admin.html         # Admin panel (backend management)
│   ├── webhook-demo.html  # API testing tool
│   ├── docs-webhook.html  # API documentation
│   ├── wiki.html          # System wiki (this page)
│   └── uploads/           # Uploaded files directory
│
└── Systemd: /etc/systemd/system/ai-orchestrator-2022.service

File Responsibilities

FileLinesRole
server.py~1800Main app: FastAPI routes, SQLAlchemy models, asyncio job queue, webhook delivery, auth middleware, SSE events, admin stats endpoints, watchdog
workers.py~1370Task dispatch: route task → backend GPU, HTTP calls, response normalization, async polling (PiAPI, FlowProxy, MiniMax)
handlers.py~620Helpers: NSFW detection, webhook retry with backoff, EMA time prediction, payload validation & building
backends.py~430Backend management: load/save config, Backend class, round-robin selection, semaphore concurrency control

Deployment & Operations

Systemd Service Configuration

[Service]
ExecStart=uvicorn server:app --host 0.0.0.0 --port 2022
         --workers 4 --loop uvloop --http httptools
         --timeout-keep-alive 75 --limit-concurrency 2000
         --backlog 8192 --proxy-headers
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=1
TimeoutStopSec=10
KillSignal=SIGINT
LimitNOFILE=65535

Performance Tuning

OptimizationDetail
uvloop + httptools~10% throughput improvement over stdlib
4 Uvicorn workersMulti-core CPU utilization
40 Job workers40 concurrent job processing
DB poolsize=20, max_overflow=40
Graceful reloadsystemctl reload = zero-downtime (SIGHUP)
Fast restart~2 seconds (SIGINT + TimeoutStopSec=10)
CTE queriesAdmin stats use Common Table Expressions

Automatic Maintenance

MechanismIntervalAction
Stuck job watchdogEvery 30sJobs running > 900s → force fail
Expired job cleanupEvery 60sJobs past expires_at → delete
Webhook log cleanupEvery 60sLogs older than 7 days → delete

Monitoring Commands

# Health check
curl http://15.235.200.66:2022/healthz

# Queue status
curl http://15.235.200.66:2022/diag/queue

# Backend health
curl http://15.235.200.66:2022/diag/backends

# Live logs
journalctl -u ai-orchestrator-2022 -f

Developer Guide

⚠ Mandatory Dev Rules (BẮT BUỘC)

Áp dụng cho mọi việc triển khai tính năng / dev / bugfix. Không được báo "pass"/"xong" nếu chưa làm đủ:

  1. Dùng Superpowers để triển khai (brainstorming → writing-plans → TDD → code-review → verification-before-completion).
  2. Audit + review + stress test thật 2 VÒNG mới được báo pass — hai vòng độc lập, có bằng chứng (output thật), không tự nhận pass khi chưa chạy kiểm chứng.
  3. Nếu có giao diện (UI): phải test bằng trình duyệt thật (Playwright/Chromium) trên cả mobile lẫn desktop thật.
  4. Mọi test phải mô phỏng như người dùng thật — đi đúng luồng người dùng, không mock cho qua.

Lý do: ưu tiên độ ổn định và trải nghiệm người dùng tuyệt đối — "người dùng là quan trọng nhất".

Adding a New Backend Node

  1. Go to Admin Panel → select category
  2. Click "Add Backend"
  3. Enter: name, URL, slots, API key (if needed)
  4. Enable → backend is ready immediately (no restart needed)

Adding a New Task Type

Step 1: Add dispatch function in workers.py

async def _dispatch_new_task(payload: dict) -> dict:
    """Call backend, return normalized result."""
    prompt = payload.get("prompt")
    if not prompt:
        raise HTTPException(400, detail="prompt required")

    backend = pick_backend("new_task")
    async with backend.sem:
        async with httpx.AsyncClient(timeout=300) as client:
            resp = await client.post(backend.url, json={"prompt": prompt})

    data = resp.json()
    return {"ok": True, "url": data["result_url"], "meta": {"backend": backend.name}}

Step 2: Register in dispatch_task()

# In workers.py, inside dispatch_task()
if task == "new_task":
    return await _dispatch_new_task(payload)

Step 3: Add fallback estimate in server.py

PREDICT_FALLBACKS = {
    ...
    "new_task": 30,  # default prediction: 30 seconds
}

Step 4: Add backend category & configure nodes via Admin UI

Processing Flow

POST /v1/jobs ↓ server.py: validate → create job in DB → push to asyncio.Queue ↓ server.py: _job_worker() picks from queue ↓ server.py: _process_job() → workers.py: dispatch_task() ↓ workers.py: pick_backend() → call GPU → normalize response ↓ server.py: update DB → send webhook → update EMA predictions

Coding Conventions

ConventionDetail
Normalized responseAll tasks return: {ok, url, filename, meta, error}
Semaphore guardAlways use async with backend.sem: for concurrency
Error handlingUse BackendHTTPException(502, message, backend_name)
Async pollingPattern: submit → poll every 3s → download → re-upload
NSFW checkAdd at start of dispatch for user-facing image tasks
RetryOnly on 502 errors (backend issues), never on 4xx (client errors)

Tip: Test new task types via API Demo page before deploying to production.