Files
lcbp3/specs/200-fullstacks/235-ai-runtime-policy-refactor/spec.md
T
admin 71c5e88181
CI / CD Pipeline / build (push) Has been skipped
CI / CD Pipeline / deploy (push) Has been skipped
690611:1705 ADR-035-235 #00 [skip CI]
2026-06-11 17:05:17 +07:00

194 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// File: specs/200-fullstacks/235-ai-runtime-policy-refactor/spec.md
// Change Log:
// - 2026-06-11: Initial specification for AI Runtime Policy Refactor (RTX 5060 Ti 16GB)
# Feature Specification: AI Runtime Policy Refactor
**Feature Branch**: `235-ai-runtime-policy-refactor`
**Created**: 2026-06-11
**Status**: Draft
**Category**: 200-fullstacks
**Input**: User description from `docs/AI-Refactor.md` + `docs/0001-ai-runtime-policy-refactor.md`
## Overview
ปรับ AI runtime ของ LCBP3-DMS ให้รองรับ GPU ใหม่ (RTX 5060 Ti 16GB) โดยนำ canonical model identities (`np-dms-ai`, `np-dms-ocr`), policy-driven `executionProfile` contract, และ LLM-First GPU ownership มาใช้แทนระบบเดิมที่ caller เลือก model/parameter เองได้ และ keep_alive แบบ fixed ค่า
อ้างอิง: `docs/AI-Refactor.md`, `docs/0001-ai-runtime-policy-refactor.md`, ADR-033, ADR-034
---
## User Scenarios & Testing _(mandatory)_
### User Story 1 — Policy Contract & Canonical Naming (Priority: P1)
นักพัฒนาและ admin ที่ส่ง AI job request ผ่าน AI Gateway จะส่งได้แค่ `executionProfile` (`fast | balanced | thai-accurate | large-context`) โดยไม่สามารถระบุชื่อ model หรือ override runtime parameters ได้เอง — system แสดงและบันทึก model ในทุก layer ด้วยชื่อ canonical `np-dms-ai` และ `np-dms-ocr` แทนชื่อ runtime เดิม
**Why this priority**: เป็นรากฐานของทุก workstream — ถ้า contract ยังเป็น caller-driven อยู่ workstream อื่นไม่มีความหมาย
**Independent Test**: ยิง POST ไปยัง AI Gateway endpoint ด้วย payload ที่มี `model.key` หรือ `temperature` แล้วตรวจว่า API reject 400 พร้อม error message; ยิงด้วย `executionProfile: "balanced"` แล้วตรวจว่าผ่านและ log/response แสดง `np-dms-ai`
**Acceptance Scenarios**:
1. **Given** AI job request ที่มี `model: { key: "typhoon2.5-np-dms:latest" }`, **When** ส่งไปยัง `POST /api/ai/jobs`, **Then** system ตอบ HTTP 400 พร้อมข้อความว่า field `model.key` ไม่อนุญาต
2. **Given** AI job request ที่มี `executionProfile: "balanced"`, **When** job ถูก dispatch ไปยัง `ai-batch` queue, **Then** job payload บันทึก `modelUsed: "np-dms-ai"` ใน audit log
3. **Given** admin เปิด AI Admin Console, **When** ดู model information panel, **Then** แสดงชื่อ `np-dms-ai` และ `np-dms-ocr` ไม่ใช่ชื่อ runtime จริง (เช่น `typhoon2.5-np-dms:latest`)
4. **Given** `auto-fill-document` job ถูกส่งมาพร้อม `executionProfile: "fast"`, **When** backend process job, **Then** backend override เป็น deterministic profile โดยไม่ใช้ค่า `fast` ที่ caller ส่งมา
5. **Given** `large-context` profile ถูกส่งโดย non-admin user, **When** backend validate, **Then** ตอบ HTTP 403 เพราะ profile นั้น restrict เฉพาะ admin/special workflows
---
### User Story 2 — Adaptive OCR Residency (Priority: P2)
Backend คำนวณ `keep_alive` value ของ `np-dms-ocr` แบบ dynamic ตาม VRAM headroom และ active workload ณ ขณะนั้น แทนการใช้ค่า fixed `keep_alive: 0` หรือ `keep_alive: 300` ตายตัว
**Why this priority**: แก้ปัญหา VRAM contention โดยตรง — ถ้า OCR ค้างอยู่ใน VRAM ตลอดจะบล็อก main model; ถ้า unload ทุกครั้งจะมี cold start penalty สูง 515 วินาที
**Independent Test**: รัน OCR job ขณะที่ `large-context` profile active และตรวจว่า `keep_alive: 0` ถูกส่งไป OCR sidecar; รัน OCR job ขณะที่ VRAM headroom สูงและตรวจว่าได้ residency window > 0
**Acceptance Scenarios**:
1. **Given** active job กำลังใช้ `large-context` profile, **When** OCR job เข้ามา, **Then** `keep_alive` ที่ส่งไป Ollama = `0`
2. **Given** ไม่มี active main model pressure และ VRAM headroom ≥ threshold, **When** OCR job เข้ามา, **Then** `keep_alive` ที่ส่งไป Ollama > `0` (residency window)
3. **Given** main model pressure สูง (high VRAM utilization), **When** OCR job เข้ามา, **Then** `keep_alive` = `0` เสมอ
4. **Given** OCR residency policy ทำงาน, **When** ดู trace/log ของ OCR request, **Then** log บันทึก residency decision พร้อม headroom value ที่ใช้ตัดสิน
---
### User Story 3 — Retrieval Acceleration with CPU Fallback (Priority: P3)
`/embed` และ `/rerank` endpoints บน OCR sidecar ตรวจสอบ VRAM headroom ก่อนใช้ GPU; ถ้า headroom ไม่ผ่าน policy threshold ให้ fallback ไป CPU ทันทีโดยไม่ fail และไม่รอ GPU queue
**Why this priority**: ป้องกัน RAG query ล้มเหลวในช่วงที่ GPU ถูกใช้งานสูง — retrieval ยังทำงานได้แค่ช้าลง
**Independent Test**: จำลอง GPU pressure สูงแล้วยิง RAG query — ตรวจว่า query ยังตอบได้ (อาจช้ากว่าปกติ) และ log บันทึก `"retrieval: cpu-fallback"`
**Acceptance Scenarios**:
1. **Given** GPU headroom < threshold, **When** `POST /embed` ถูกเรียก, **Then** ใช้ CPU compute โดยไม่ return error
2. **Given** GPU headroom < threshold, **When** `POST /rerank` ถูกเรียก, **Then** ใช้ CPU compute และ response ปกติ
3. **Given** fallback เกิดขึ้น, **When** ดู sidecar log, **Then** log entry มี `device: "cpu"` และ `reason: "gpu-headroom-below-threshold"`
4. **Given** `rag-query` job รัน, **When** GPU ถูก main model ใช้งานอยู่, **Then** RAG query ยังตอบ response ได้ (ไม่ timeout หรือ fail hard)
---
### User Story 4 — Queue Policy & Selective Realtime Concurrency (Priority: P4)
BullMQ queue ปรับให้ `ai-realtime` รองรับ concurrency = 2 ได้เฉพาะ lightweight realtime jobs (intent classification ที่ไม่เรียก OCR, tool-only suggestion ที่ไม่ต้อง model switching) โดยยังคง pause/resume coordination เดิม และ `rag-query` ยังถูก classify เป็น generation-centric job ที่อยู่ใน `ai-batch`
**Why this priority**: เพิ่ม throughput ให้ lightweight jobs โดยไม่กระทบ GPU safety
**Independent Test**: ส่ง intent classification job 2 อันพร้อมกัน ตรวจว่าทั้งสองรันพร้อมกันได้ใน `ai-realtime`; ส่ง `rag-query` ตรวจว่าไปอยู่ใน `ai-batch` ไม่ใช่ `ai-realtime`
**Acceptance Scenarios**:
1. **Given** 2 intent classification jobs เข้ามาพร้อมกัน, **When** ทั้งคู่ถูก dispatch, **Then** ทั้งคู่ process พร้อมกันใน `ai-realtime` queue (concurrency = 2)
2. **Given** `rag-query` job เข้ามา, **When** dispatch, **Then** job ถูกส่งไป `ai-batch` queue ไม่ใช่ `ai-realtime`
3. **Given** `ai-batch` ถูก pause เนื่องจาก realtime pressure, **When** pause/resume coordination ทำงาน, **Then** `ai-realtime` ยังคง concurrency = 2 ได้สำหรับ lightweight jobs
---
### User Story 5 — Verification & Cutover Gate (Priority: P5)
ระบบมี automated tests และ manual validation checklist ครบตามทั้ง 4 แกน (policy contract, canonical naming, adaptive OCR residency, retrieval fallback) ก่อนถือว่า big bang cutover สำเร็จ — ไม่อนุญาต partial success
**Why this priority**: เป็น safety net ของทั้ง refactor — partial cutover อาจทำให้ระบบ inconsistent
**Independent Test**: รัน test suite ที่ครอบคลุมทั้ง 4 แกนแล้วทุก test ผ่าน; admin สามารถเปิด AI Admin Console และ OCR Sandbox ตรวจ label/behavior จริงได้
**Acceptance Scenarios**:
1. **Given** test suite รัน, **When** ทุก test ผ่าน, **Then** cutover gate ถือว่าผ่านในส่วน executable verification
2. **Given** admin เปิด AI Admin Console, **When** ดู model labels ทั้งหมด, **Then** ไม่มีชื่อ `typhoon2.5-np-dms:latest` หรือ `typhoon-np-dms-ocr:latest` ปรากฏใน UI
3. **Given** admin รัน OCR Sandbox ซ้ำหลาย job ในเงื่อนไข headroom ต่างกัน, **When** ดู behavior, **Then** `keep_alive` ต่างกันตาม policy ที่ defined
---
### Edge Cases
- ถ้า VRAM headroom calculation service ล้มเหลว (timeout หรือ error) → ต้อง fallback เป็น `keep_alive: 0` เสมอ (safe default)
- ถ้า caller ส่ง `executionProfile` ที่ไม่อยู่ใน canonical set → ตอบ 400 validation error
- ถ้า `large-context` profile ถูก whitelist ให้ admin แต่ VRAM ไม่พอ → backend ต้อง reject พร้อม error ชัดเจน ไม่ใช่ silent fallback
- ถ้า OCR job เข้ามาพร้อมกับ main model generation job → LLM-First rule บังคับ: OCR ต้องรอหรือใช้ `keep_alive: 0`
- ถ้า `/embed` fallback ไป CPU แล้ว job ใช้เวลานานเกิน timeout → ต้อง return partial result หรือ error ที่ชัดเจน ไม่ใช่ hang
---
## Requirements _(mandatory)_
### Functional Requirements
**Workstream A: Contract & Canonical Naming**
- **FR-A01**: System MUST reject AI job requests ที่มี `model.key` field ใน payload (HTTP 400)
- **FR-A02**: System MUST reject AI job requests ที่มี direct `temperature`, `top_p`, หรือ `maxTokens` overrides (HTTP 400)
- **FR-A03**: `executionProfile` MUST รับค่าได้เฉพาะ `fast | balanced | thai-accurate | large-context`
- **FR-A04**: `large-context` profile MUST ถูก authorize เฉพาะ admin role หรือ backend-whitelisted workflows
- **FR-A05**: System MUST map `executionProfile` → canonical model name และ runtime parameters ใน backend policy layer
- **FR-A06**: งาน data-affecting (`migrate-document`, `auto-fill-document`) MUST ถูก backend override profile โดยไม่ใช้ค่าที่ caller ส่งมา
- **FR-A07**: ทุก layer (API response, audit log, Admin Console, OCR Sandbox) MUST แสดงชื่อ `np-dms-ai` และ `np-dms-ocr` แทนชื่อ runtime จริง
**Workstream B: Runtime Policy**
- **FR-B01**: Backend MUST มี policy mapping: `executionProfile``{ canonicalModel, keep_alive, temperature, top_p, maxTokens }`
- **FR-B02**: OCR residency MUST คำนวณ `keep_alive` แบบ dynamic จาก VRAM headroom และ active profile
- **FR-B03**: ถ้า active profile = `large-context` หรือ main model pressure = high → OCR `keep_alive` MUST = `0`
- **FR-B04**: ถ้า VRAM headroom ≥ policy threshold → OCR สามารถใช้ residency window > 0
- **FR-B05**: VRAM headroom calculation ล้มเหลว → MUST fallback เป็น `keep_alive: 0` (safe default)
- **FR-B06**: OCR residency decision MUST ถูก log พร้อม headroom value ที่ใช้ตัดสิน
**Workstream C: Retrieval Acceleration**
- **FR-C01**: `/embed` endpoint MUST ตรวจ VRAM headroom ก่อน GPU compute; ถ้าไม่ผ่าน → fallback CPU
- **FR-C02**: `/rerank` endpoint MUST ตรวจ VRAM headroom ก่อน GPU compute; ถ้าไม่ผ่าน → fallback CPU
- **FR-C03**: CPU fallback MUST ไม่ hard fail และ MUST ไม่รอ GPU queue — ถ้า CPU compute timeout ต้อง return HTTP 504 พร้อม error message ชัดเจน (ไม่ return partial result)
- **FR-C04**: Fallback event MUST ถูก log พร้อม `device: "cpu"` และ `reason`
- **FR-C05**: `rag-query` job MUST ยังตอบได้เมื่อ GPU retrieval path ถูก fallback ไป CPU
- **FR-C06**: VRAM headroom threshold MUST เป็น configurable env variable (`VRAM_HEADROOM_THRESHOLD_MB`) — ถ้า VRAM query ล้มเหลว ให้ใช้ safe default = 0 MB (บังคับ fallback)
**Workstream D: Queue Policy**
- **FR-D01**: `ai-realtime` queue MUST รองรับ concurrency = 2 สำหรับ lightweight realtime jobs
- **FR-D02**: Lightweight realtime jobs ได้แก่: intent classification (ไม่เรียก OCR), tool-only suggestion (ไม่ต้อง model switching)
- **FR-D03**: `rag-query` MUST ถูก dispatch ไป `ai-batch` ไม่ใช่ `ai-realtime`
- **FR-D04**: pause/resume coordination ระหว่าง `ai-realtime` และ `ai-batch` MUST ยังคงทำงานได้ตามเดิม
### Key Entities
- **ExecutionProfile**: Enum value ที่ caller ส่งมา (`fast | balanced | thai-accurate | large-context`) — contract ระดับ API
- **RuntimePolicy**: Backend mapping จาก `ExecutionProfile``{ canonicalModel, keep_alive, temperature, top_p, maxTokens }` — ไม่ expose ใน API
- **VramHeadroom**: ค่า computed ณ เวลา request ที่ใช้ตัดสิน OCR residency และ retrieval acceleration — บันทึกใน log
- **CanonicalModelIdentity**: ชื่อ `np-dms-ai` หรือ `np-dms-ocr` — ใช้ทุกชั้นที่ผู้ใช้เห็น
- **OcrResidencyDecision**: ผลการคำนวณ `keep_alive` value สำหรับ OCR job แต่ละครั้ง — บันทึกใน log พร้อม input factors
---
## Success Criteria _(mandatory)_
### Measurable Outcomes
- **SC-001**: AI job requests ที่มี `model.key` หรือ parameter overrides ถูก reject 100% ในทุก environment
- **SC-002**: ทุก layer ที่ผู้ใช้และนักพัฒนาเห็น (API response, audit log, Admin Console, OCR Sandbox) แสดงชื่อ `np-dms-ai` / `np-dms-ocr` 100% โดยไม่มีชื่อ runtime รั่วออกมา
- **SC-003**: OCR cold start penalty ลดลงจากการใช้ adaptive residency ในสถานการณ์ที่ VRAM headroom เพียงพอ (วัดจาก average OCR latency ใน non-contention scenario)
- **SC-004**: RAG query ยังตอบ response ได้ 100% แม้ GPU retrieval path ถูก fallback ไป CPU (ไม่มี hard failure)
- **SC-005**: Automated test suite ครอบคลุมทั้ง 4 แกนของ cutover gate ผ่าน 100%
- **SC-006**: lightweight realtime job throughput เพิ่มขึ้น (สามารถ process 2 concurrent lightweight jobs) ขณะที่ pause/resume coordination ยังทำงานได้
---
## Clarifications
### Session 2026-06-11
- Q: ถ้า `/embed` fallback ไป CPU แล้ว job ใช้เวลานานเกิน timeout → ควร return partial result หรือ return error ที่ชัดเจน? → A: Return error ที่ชัดเจนพร้อม HTTP 504 timeout message — ไม่ return partial result เพราะ downstream LLM context จะ incomplete และทำให้ผลลัพธ์ผิดพลาดโดยไม่รู้ตัว
- Q: VRAM headroom threshold ระดับ spec ควรกำหนด default value ไหม? → A: ไม่กำหนดใน spec — threshold เป็น operational config (env variable `VRAM_HEADROOM_THRESHOLD_MB`) ที่ ops/admin ปรับได้ runtime; spec ระบุแค่ว่า "ต้องมี threshold ที่ configurable" และ "ต้องใช้ safe default = 0 (unload) เมื่อ query ล้มเหลว"
## Assumptions
- GPU ปัจจุบัน (RTX 5060 Ti 16GB) รองรับ VRAM monitoring API ที่ Ollama หรือ sidecar สามารถ query ได้
- VRAM headroom threshold ค่าเริ่มต้นจะถูกกำหนดใน config/env และปรับได้โดยไม่ต้อง redeploy
- Canonical model names (`np-dms-ai`, `np-dms-ocr`) ถูก tag ใน Ollama registry บน Desk-5439 ก่อน cutover
- OCR sidecar (`app.py`) บน Desk-5439 จะถูก update เป็นส่วนหนึ่งของ cutover
- Big bang rollout: ไม่มี parallel legacy path — ทุก change deploy พร้อมกันในรอบเดียว
- `ai-realtime` concurrency uplift เป็น configuration change ไม่ใช่ architectural change ใหม่