Instructions to use Gavin-chen/Qwen-Compress with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Gavin-chen/Qwen-Compress with Transformers:
# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Gavin-chen/Qwen-Compress", dtype="auto") - Notebooks
- Google Colab
- Kaggle
| import os | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import uvicorn | |
| import threading | |
| import time | |
| import uuid | |
| import json | |
| from fastapi import FastAPI, Request, HTTPException | |
| from fastapi.responses import JSONResponse | |
| from fastapi.responses import StreamingResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, Field, ConfigDict | |
| from typing import List, Optional, Union, Dict, Any | |
| from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer | |
| from transformers import BitsAndBytesConfig | |
| from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training | |
| from torch.utils.checkpoint import checkpoint | |
| from transformers.modeling_outputs import CausalLMOutputWithPast | |
| import asyncio | |
| # ========================================== | |
| # 1. 初始化 FastAPI App 與 跨域(CORS) 設定 | |
| # ========================================== | |
| app = FastAPI(title="OpenAI-Compatible FrankenQwen Server", version="1.0") | |
| # 開啟 CORS,讓前端 UI (如 Open WebUI) 可以順利連線 | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| MAX_CONCURRENT_REQUESTS = 4 | |
| gpu_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS) | |
| def get_vram_status(): | |
| import torch | |
| if not torch.cuda.is_available(): | |
| return {"status": "CUDA 不可用"} | |
| # 轉換為 GiB (除以 1024^3) | |
| allocated = torch.cuda.memory_allocated() / (1024 ** 3) | |
| reserved = torch.cuda.memory_reserved() / (1024 ** 3) | |
| peak = torch.cuda.max_memory_allocated() / (1024 ** 3) | |
| return { | |
| "current_tensor_used_gib": round(allocated, 2), | |
| "pytorch_reserved_gib": round(reserved, 2), | |
| "peak_vram_used_gib": round(peak, 2) | |
| } | |
| # ========================================== | |
| # 2. 原始模型組件與邏輯 (保持不變) | |
| # ========================================== | |
| class HiddenStateCompressor(nn.Module): | |
| def __init__(self, dim, ratio=4): | |
| super().__init__() | |
| self.ratio = ratio | |
| self.wgate = nn.Linear(dim, 1, bias=False) | |
| self.ape = nn.Parameter(torch.empty(ratio, 1)) | |
| nn.init.normal_(self.ape, std=0.02) | |
| def forward(self, x): | |
| B, L, D = x.shape | |
| assert L % self.ratio == 0, "輸入長度必須是壓縮倍率的整數倍" | |
| orig_dtype = x.dtype | |
| target_dtype = self.wgate.weight.dtype | |
| x_reshaped = x.view(B, L // self.ratio, self.ratio, D).to(target_dtype) | |
| scores = self.wgate(x_reshaped) + self.ape.view(1, 1, self.ratio, 1) | |
| scores_fp32 = scores.to(torch.float32) | |
| scores_probs = F.softmax(scores_fp32, dim=2).to(target_dtype) | |
| compressed_x = (x_reshaped * scores_probs).sum(dim=2) | |
| return compressed_x.to(orig_dtype) | |
| def get_custom_qwen_forward(compressor_module, window_size=128, ratio=4): | |
| from collections.abc import Callable | |
| from transformers.models.qwen3_5.modeling_qwen3_5 import ALL_ATTENTION_FUNCTIONS, apply_rotary_pos_emb, eager_attention_forward, FlashAttentionKwargs | |
| from transformers.processing_utils import Unpack | |
| from transformers.cache_utils import Cache | |
| def forward2( | |
| self, hidden_states: torch.Tensor, position_embeddings: tuple[torch.Tensor, torch.Tensor], | |
| attention_mask: torch.Tensor | None, past_key_values: Cache | None = None, **kwargs: Unpack[FlashAttentionKwargs], | |
| ) -> tuple[torch.Tensor, torch.Tensor | None]: | |
| cos, sin = position_embeddings | |
| bsz, q_len, _ = hidden_states.size() | |
| input_shape = hidden_states.shape[:-1] | |
| hidden_shape = (*input_shape, -1, self.head_dim) | |
| hidden_states = hidden_states.contiguous() | |
| query_states, gate = torch.chunk( | |
| self.q_proj(hidden_states).view(*input_shape, -1, self.head_dim * 2), 2, dim=-1 | |
| ) | |
| gate = gate.contiguous().reshape(*input_shape, -1) | |
| query_states = self.q_norm(query_states.contiguous().view(hidden_shape)).transpose(1, 2) | |
| key_states, value_states = None, None | |
| do_compress = getattr(self, "compress_enabled", True) | |
| if do_compress: | |
| if q_len > 1: # Prefill | |
| window = min(window_size, q_len) | |
| global_len = q_len - window | |
| cutoff = global_len - (global_len % ratio) | |
| compressed = compressor_module(hidden_states[:, :cutoff, :]) if cutoff > 0 else hidden_states[:, :0, :] | |
| residual = hidden_states[:, cutoff:global_len, :] if global_len > cutoff else hidden_states[:, :0, :] | |
| tail = hidden_states[:, global_len:, :] | |
| kv_hidden = torch.cat([compressed, residual, tail], dim=1).contiguous() | |
| compressed_pos = torch.arange(ratio - 1, cutoff, ratio, device=hidden_states.device, dtype=torch.long) if cutoff > 0 else torch.arange(0, device=hidden_states.device, dtype=torch.long) | |
| residual_pos = torch.arange(cutoff, global_len, device=hidden_states.device, dtype=torch.long) | |
| tail_pos = torch.arange(global_len, q_len, device=hidden_states.device, dtype=torch.long) | |
| kv_positions = torch.cat([compressed_pos, residual_pos, tail_pos], dim=0) | |
| kv_seq_len = kv_positions.size(0) | |
| q_pos = torch.arange(q_len, device=hidden_states.device).view(q_len, 1) | |
| k_pos = kv_positions.view(1, kv_seq_len) | |
| custom_causal_mask = (q_pos >= k_pos) | |
| additive_mask = torch.zeros((q_len, kv_seq_len), dtype=hidden_states.dtype, device=hidden_states.device) | |
| additive_mask.masked_fill_(~custom_causal_mask, torch.finfo(hidden_states.dtype).min) | |
| additive_mask = additive_mask.view(1, 1, q_len, kv_seq_len).expand(bsz, 1, q_len, kv_seq_len) | |
| attention_mask = (attention_mask[..., kv_positions] + additive_mask) if attention_mask is not None else additive_mask | |
| kv_hidden_shape = (bsz, kv_seq_len, -1, self.head_dim) | |
| key_states = self.k_norm(self.k_proj(kv_hidden).view(kv_hidden_shape)).transpose(1, 2).contiguous() | |
| value_states = self.v_proj(kv_hidden).view(kv_hidden_shape).transpose(1, 2).contiguous() | |
| query_states, _ = apply_rotary_pos_emb(query_states, query_states, cos, sin) | |
| k_cos = cos[:, kv_positions, :] if cos.dim() == 3 else cos[kv_positions, :] | |
| k_sin = sin[:, kv_positions, :] if sin.dim() == 3 else sin[kv_positions, :] | |
| _, key_states = apply_rotary_pos_emb(key_states, key_states, k_cos, k_sin) | |
| if past_key_values is not None: | |
| key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx) | |
| else: # Decode | |
| new_key = self.k_norm(self.k_proj(hidden_states).view(bsz, 1, -1, self.head_dim)).transpose(1, 2) | |
| new_value = self.v_proj(hidden_states).view(bsz, 1, -1, self.head_dim).transpose(1, 2) | |
| query_states, _ = apply_rotary_pos_emb(query_states, query_states, cos, sin) | |
| _, new_key = apply_rotary_pos_emb(new_key, new_key, cos, sin) | |
| target_dtype = hidden_states.dtype | |
| new_key = new_key.to(target_dtype).contiguous() | |
| new_value = new_value.to(target_dtype).contiguous() | |
| if past_key_values is not None: | |
| if not hasattr(past_key_values, "uncompressed_buffer_k"): | |
| past_key_values.uncompressed_buf_h = {} | |
| past_key_values.uncompressed_buffer_k = {} | |
| past_key_values.uncompressed_buffer_v = {} | |
| layer_idx = self.layer_idx | |
| buf_k = past_key_values.uncompressed_buffer_k.get(layer_idx, torch.empty((bsz, self.config.num_key_value_heads, 0, self.head_dim), device=new_key.device, dtype=target_dtype)) | |
| buf_v = past_key_values.uncompressed_buffer_v.get(layer_idx, torch.empty((bsz, self.config.num_key_value_heads, 0, self.head_dim), device=new_value.device, dtype=target_dtype)) | |
| buf_h = past_key_values.uncompressed_buf_h.get(layer_idx, torch.empty((bsz, 0, hidden_states.size(-1)), device=hidden_states.device, dtype=hidden_states.dtype)) | |
| buf_h = torch.cat([buf_h, hidden_states], dim=1) | |
| buf_k = torch.cat([buf_k, new_key], dim=2) | |
| buf_v = torch.cat([buf_v, new_value], dim=2) | |
| buffer_len = buf_k.shape[2] | |
| if getattr(self, "compress_enabled", True) and buffer_len >= (window_size + ratio): | |
| to_compress_h = buf_h[:, :ratio, :] | |
| compressed_h = compressor_module(to_compress_h) | |
| compressed_k = self.k_norm(self.k_proj(compressed_h).view(bsz, 1, -1, self.head_dim)).transpose(1, 2) | |
| compressed_v = self.v_proj(compressed_h).view(bsz, 1, -1, self.head_dim).transpose(1, 2) | |
| past_key_values.update(compressed_k, compressed_v, layer_idx) | |
| buf_h = buf_h[:, ratio:, :] | |
| buf_k = buf_k[:, :, ratio:, :] | |
| buf_v = buf_v[:, :, ratio:, :] | |
| past_key_values.uncompressed_buffer_k[layer_idx] = buf_k | |
| past_key_values.uncompressed_buffer_v[layer_idx] = buf_v | |
| past_key_values.uncompressed_buf_h[layer_idx] = buf_h | |
| layer_cache = past_key_values.layers[layer_idx] if hasattr(past_key_values, "layers") else None | |
| history_k = layer_cache.keys if layer_cache else past_key_values.key_cache[layer_idx] | |
| history_v = layer_cache.values if layer_cache else past_key_values.value_cache[layer_idx] | |
| key_states = torch.cat([history_k, buf_k], dim=2) if history_k is not None and history_k.shape[2] > 0 else buf_k | |
| value_states = torch.cat([history_v, buf_v], dim=2) if history_v is not None and history_v.shape[2] > 0 else buf_v | |
| kv_seq_len = past_key_values.get_seq_length(self.layer_idx) + buf_k.shape[2] | |
| else: | |
| key_states, value_states = new_key, new_value | |
| else: | |
| kv_hidden_shape = (bsz, q_len, -1, self.head_dim) | |
| key_states = self.k_norm(self.k_proj(hidden_states).view(kv_hidden_shape)).transpose(1, 2).contiguous() | |
| value_states = self.v_proj(hidden_states).view(kv_hidden_shape).transpose(1, 2).contiguous() | |
| query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) | |
| if past_key_values is not None: | |
| key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx) | |
| kv_seq_len = key_states.shape[2] | |
| target_dtype = hidden_states.dtype | |
| query_states, key_states, value_states = query_states.to(target_dtype).contiguous(), key_states.to(target_dtype).contiguous(), value_states.to(target_dtype).contiguous() | |
| attention_interface: Callable = ALL_ATTENTION_FUNCTIONS.get_interface(self.config._attn_implementation, eager_attention_forward) | |
| attn_output, attn_weights = attention_interface( | |
| self, query_states, key_states, value_states, attention_mask, | |
| dropout=0.0 if not self.training else self.attention_dropout, scaling=self.scaling, **kwargs, | |
| ) | |
| attn_output = attn_output.reshape(*input_shape, -1).contiguous() | |
| attn_output = attn_output * torch.sigmoid(gate) | |
| return self.o_proj(attn_output), attn_weights | |
| return forward2 | |
| def get_chunked_causal_forward(chunk_size=512): | |
| def custom_causal_forward(self, input_ids=None, attention_mask=None, position_ids=None, past_key_values=None, inputs_embeds=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, **kwargs): | |
| outputs = self.model(input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids, past_key_values=past_key_values, inputs_embeds=inputs_embeds, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, **kwargs) | |
| hidden_states = outputs[0] | |
| loss, logits = None, None | |
| if labels is not None: | |
| shift_hidden_states = hidden_states[..., :-1, :].contiguous() | |
| shift_labels = labels[..., 1:].contiguous() | |
| seq_len = shift_hidden_states.shape[1] | |
| total_loss = 0.0 | |
| active_tokens = (shift_labels != -100).sum().clamp(min=1) | |
| def compute_chunk_loss(h_chunk, lbl_chunk): | |
| c_logits = self.lm_head(h_chunk).float() | |
| return F.cross_entropy(c_logits.reshape(-1, c_logits.size(-1)), lbl_chunk.reshape(-1), reduction='sum') | |
| for i in range(0, seq_len, chunk_size): | |
| end_i = min(i + chunk_size, seq_len) | |
| c_loss = checkpoint(compute_chunk_loss, shift_hidden_states[:, i:end_i, :], shift_labels[:, i:end_i], use_reentrant=False) | |
| total_loss += c_loss | |
| loss = total_loss / active_tokens | |
| else: | |
| logits = self.lm_head(hidden_states) | |
| return CausalLMOutputWithPast(loss=loss, logits=logits, past_key_values=outputs.past_key_values, hidden_states=outputs.hidden_states, attentions=outputs.attentions) | |
| return custom_causal_forward | |
| def load_custom_checkpoint(model, filepath="/vault/franken_qwen2B_compressor_checkpoint.pt"): | |
| if not os.path.exists(filepath): | |
| print(f"⚠️ 找不到權重檔 {filepath}!將使用原始權重。") | |
| return model | |
| print(f"\n📂 正在載入客製化權重: {filepath}") | |
| trainable_state_dict = torch.load(filepath, map_location="cpu", weights_only=True) | |
| clean_state_dict = { (k[7:] if k.startswith('module.') else k): v for k, v in trainable_state_dict.items() } | |
| model.load_state_dict(clean_state_dict, strict=False) | |
| print("✅ 客製化權重完美掛載!") | |
| return model | |
| def build_franken_qwen(qwen_id="Qwen/Qwen3.6-27B", device="cuda"): | |
| quantization_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16) | |
| qwen_model = AutoModelForCausalLM.from_pretrained(qwen_id, dtype=torch.bfloat16, device_map="auto", quantization_config=quantization_config) | |
| #qwen_model = prepare_model_for_kbit_training(qwen_model) | |
| qwen_model.compressors = nn.ModuleList() | |
| for i, layer in enumerate(qwen_model.model.layers): | |
| if i % 4 == 3: | |
| compressor = HiddenStateCompressor(dim=qwen_model.config.hidden_size, ratio=4).to(device, dtype=torch.bfloat16) | |
| qwen_model.compressors.append(compressor) | |
| custom_fw = get_custom_qwen_forward(compressor, window_size=128, ratio=4) | |
| layer.self_attn.forward = custom_fw.__get__(layer.self_attn, type(layer.self_attn)) | |
| qwen_model.forward = get_chunked_causal_forward(chunk_size=512).__get__(qwen_model, type(qwen_model)) | |
| for param in qwen_model.parameters(): param.requires_grad = False | |
| for compressor in qwen_model.compressors: | |
| for param in compressor.parameters(): param.requires_grad = True | |
| if qwen_id=="Qwen/Qwen3.6-27B": | |
| lora_config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") | |
| qwen_model = get_peft_model(qwen_model, lora_config) | |
| return load_custom_checkpoint(qwen_model, filepath="./Downloads/franken_qwen27B_compressor_lora.pt") | |
| elif qwen_id=="Qwen/Qwen3.5-2B": | |
| return load_custom_checkpoint(qwen_model, filepath="./Downloads/franken_qwen2B_compressor_checkpoint.pt") | |
| qwen_model.enable_input_require_grads() | |
| return load_custom_checkpoint(qwen_model, filepath="/vault/franken_qwen27B_compressor_lora.pt") | |
| # 全域模型初始化 | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| MODEL_ID = "Qwen/Qwen3.5-2B" | |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) | |
| model = build_franken_qwen(qwen_id=MODEL_ID, device="cuda") | |
| model=model.get_base_model() if hasattr(model, "get_base_model") else model | |
| model=torch.compile(model) | |
| model.eval() | |
| print(get_vram_status()) | |
| # ========================================== | |
| # 3. OpenAI 官方相容的 Pydantic 資料模型定義 | |
| # ========================================== | |
| class ChatMessage(BaseModel): | |
| role: str | |
| content: str | |
| class ChatCompletionRequest(BaseModel): | |
| # 🌟 關鍵優化:extra="allow" 允許 lm-eval 傳入額外的 OpenAI 參數 (如 n, logprobs) 而不報錯 | |
| model_config = ConfigDict(extra="allow") | |
| model: str | |
| messages: List[ChatMessage] | |
| temperature: Optional[float] = 0.7 | |
| top_p: Optional[float] = 0.9 | |
| max_tokens: Optional[int] = Field(default=512, alias="max_tokens") | |
| stream: Optional[bool] = False | |
| stop: Optional[Union[str, List[str]]] = None # 🌟 接收 lm-eval 的停止符號 | |
| class CompletionRequest(BaseModel): | |
| # 🌟 針對標準文字補全的 Request Schema | |
| model_config = ConfigDict(extra="allow") | |
| model: str | |
| prompt: Union[str, List[str]] | |
| temperature: Optional[float] = 0.7 | |
| top_p: Optional[float] = 0.9 | |
| max_tokens: Optional[int] = Field(default=512, alias="max_tokens") | |
| stream: Optional[bool] = False | |
| stop: Optional[Union[str, List[str]]] = None | |
| # ========================================== | |
| # 4. OpenAI 相容核心端點 (/v1/chat/completions) | |
| # ========================================== | |
| async def universal_exception_handler(request: Request, exc: Exception): | |
| return JSONResponse( | |
| status_code=500, | |
| content={"error": {"message": f"Internal Server Error: {str(exc)}", "type": "server_error"}} | |
| ) | |
| def prepare_gen_config(request, inputs): | |
| """公用生成參數建立函式""" | |
| gen_config = { | |
| **inputs, | |
| "max_new_tokens": request.max_tokens, | |
| "do_sample": True if request.temperature > 0 else False, | |
| "temperature": request.temperature if request.temperature > 0 else None, | |
| "top_p": request.top_p, | |
| "repetition_penalty": 1.05, | |
| "use_cache": True, | |
| "pad_token_id": tokenizer.eos_token_id, | |
| "eos_token_id": [tokenizer.eos_token_id, tokenizer.convert_tokens_to_ids("<|im_end|>")] | |
| } | |
| # 🌟 處理 lm-eval 傳過來的 stop 截斷字串 | |
| if request.stop: | |
| gen_config["stop_strings"] = [request.stop] if isinstance(request.stop, str) else request.stop | |
| gen_config["tokenizer"] = tokenizer | |
| return gen_config | |
| async def chat_completions_endpoint(request: ChatCompletionRequest): | |
| async with gpu_semaphore: # 佇列鎖管控 | |
| formatted_messages = [{"role": msg.role, "content": msg.content} for msg in request.messages] | |
| input_text = tokenizer.apply_chat_template(formatted_messages, tokenize=False, add_generation_prompt=True) | |
| inputs = tokenizer(input_text, return_tensors="pt", add_special_tokens=False).to(model.device) | |
| gen_config = prepare_gen_config(request, inputs) | |
| streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) | |
| gen_config["streamer"] = streamer | |
| threading.Thread(target=lambda: torch.no_grad()(model.generate)(**gen_config)).start() | |
| if not request.stream: | |
| # 🌟 優化:將同步阻塞的 "".join 丟進線程池,解放 Event Loop | |
| full_text = await asyncio.to_thread(lambda: "".join([new_text for new_text in streamer])) | |
| return { | |
| "id": f"chatcmpl-{uuid.uuid4()}", "object": "chat.completion", "created": int(time.time()), "model": request.model, | |
| "choices": [{"index": 0, "message": {"role": "assistant", "content": full_text}, "finish_reason": "stop"}], | |
| "usage": {"prompt_tokens": inputs["input_ids"].shape[1], "completion_tokens": len(tokenizer.encode(full_text)), "total_tokens": inputs["input_ids"].shape[1] + len(tokenizer.encode(full_text))} | |
| } | |
| else: | |
| async def openai_chat_stream(): | |
| chunk_id, created_time = f"chatcmpl-{uuid.uuid4()}", int(time.time()) | |
| yield f"data: {json.dumps({'id': chunk_id, 'object': 'chat.completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'index': 0, 'delta': {'role': 'assistant'}, 'finish_reason': None}]}, ensure_ascii=False)}\n\n" | |
| # 🌟 優化:建立安全不阻塞事件循環的取 Token 函式 | |
| def get_next_token(): | |
| try: | |
| return next(streamer), False | |
| except StopIteration: | |
| return "", True | |
| while True: | |
| # 🌟 關鍵:每次拿 Token 都讓出主執行緒,讓監控端點可以插隊進來執行 | |
| new_text, is_done = await asyncio.to_thread(get_next_token) | |
| if is_done: | |
| break | |
| if new_text: | |
| yield f"data: {json.dumps({'id': chunk_id, 'object': 'chat.completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'index': 0, 'delta': {'content': new_text}, 'finish_reason': None}]}, ensure_ascii=False)}\n\n" | |
| yield f"data: {json.dumps({'id': chunk_id, 'object': 'chat.completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]}, ensure_ascii=False)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse(openai_chat_stream(), media_type="text/event-stream") | |
| # ----- 端點 B:文字接龍 (/v1/completions) ----- | |
| async def completions_endpoint(request: CompletionRequest): | |
| async with gpu_semaphore: # 佇列鎖管控 | |
| input_text = request.prompt[0] if isinstance(request.prompt, list) else request.prompt | |
| inputs = tokenizer(input_text, return_tensors="pt", add_special_tokens=False).to(model.device) | |
| gen_config = prepare_gen_config(request, inputs) | |
| streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) | |
| gen_config["streamer"] = streamer | |
| threading.Thread(target=lambda: torch.no_grad()(model.generate)(**gen_config)).start() | |
| if not request.stream: | |
| full_text = "".join([new_text for new_text in streamer]) | |
| return { | |
| "id": f"cmpl-{uuid.uuid4()}", "object": "text_completion", "created": int(time.time()), "model": request.model, | |
| "choices": [{"text": full_text, "index": 0, "logprobs": None, "finish_reason": "stop"}], | |
| "usage": {"prompt_tokens": inputs["input_ids"].shape[1], "completion_tokens": len(tokenizer.encode(full_text)), "total_tokens": inputs["input_ids"].shape[1] + len(tokenizer.encode(full_text))} | |
| } | |
| else: | |
| async def openai_text_stream(): | |
| chunk_id, created_time = f"cmpl-{uuid.uuid4()}", int(time.time()) | |
| for new_text in streamer: | |
| if new_text: | |
| yield f"data: {json.dumps({'id': chunk_id, 'object': 'text_completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'text': new_text, 'index': 0, 'logprobs': None, 'finish_reason': None}]}, ensure_ascii=False)}\n\n" | |
| yield f"data: {json.dumps({'id': chunk_id, 'object': 'text_completion.chunk', 'created': created_time, 'model': request.model, 'choices': [{'text': '', 'index': 0, 'logprobs': None, 'finish_reason': 'stop'}]}, ensure_ascii=False)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse(openai_text_stream(), media_type="text/event-stream") | |
| # ========================================== | |
| # 5. 補齊 OpenAI 的 /v1/models 查詢端點 (許多 UI 啟動時會先呼叫這個) | |
| # ========================================== | |
| async def list_models(): | |
| return { | |
| "object": "list", | |
| "data": [ | |
| { | |
| "id": MODEL_ID, | |
| "object": "model", | |
| "created": int(time.time()), | |
| "owned_by": "custom" | |
| } | |
| ] | |
| } | |
| async def vram_monitor(): | |
| """隨時動態查詢當前 GPU 記憶體狀態""" | |
| return { | |
| "object": "vram_status", | |
| "data": get_vram_status() | |
| } | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="127.0.0.1", port=8001) |