mirror of
https://github.com/RYDE-WORK/lnp_ml.git
synced 2026-10-05 15:33:12 +08:00
fix: bound LLM inference memory for batch screening
This commit is contained in:
parent
a7c7d54c61
commit
c0d2c008b0
@ -36,8 +36,8 @@ COPY app/ ./app/
|
|||||||
# 安装项目包
|
# 安装项目包
|
||||||
RUN pip install -e .
|
RUN pip install -e .
|
||||||
|
|
||||||
# 复制模型文件
|
# 模型、Qwen 权重和 RAG 数据在运行时以只读 volume 挂载。
|
||||||
COPY models/final/ ./models/final/
|
# 不把数 GB 权重烘进镜像,也避免误把 Git LFS pointer 当成模型。
|
||||||
|
|
||||||
# ============ API 服务 ============
|
# ============ API 服务 ============
|
||||||
FROM base AS api
|
FROM base AS api
|
||||||
@ -60,4 +60,3 @@ ENV STREAMLIT_SERVER_PORT=8501 \
|
|||||||
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
|
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
|
||||||
|
|
||||||
CMD ["streamlit", "run", "app/app.py"]
|
CMD ["streamlit", "run", "app/app.py"]
|
||||||
|
|
||||||
|
|||||||
59
SERVER_DEPLOY.md
Normal file
59
SERVER_DEPLOY.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# GPU 服务器测试与部署
|
||||||
|
|
||||||
|
这个代码包不包含大模型权重。解压后,请先在项目根目录放好以下运行时文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
models/final/model.pt
|
||||||
|
models/mpnn/all_amine_split_for_LiON/cv_0/fold_0/model_0/model.pt
|
||||||
|
models/mpnn/all_amine_split_for_LiON/cv_1/fold_0/model_0/model.pt
|
||||||
|
models/mpnn/all_amine_split_for_LiON/cv_2/fold_0/model_0/model.pt
|
||||||
|
models/mpnn/all_amine_split_for_LiON/cv_3/fold_0/model_0/model.pt
|
||||||
|
models/mpnn/all_amine_split_for_LiON/cv_4/fold_0/model_0/model.pt
|
||||||
|
models/qwen2.5-7b-instruct/config.json
|
||||||
|
models/qwen2.5-7b-instruct/ # 其余 tokenizer/权重文件
|
||||||
|
data/interim/internal.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
`.pt` 必须是真实二进制权重,不能是 100 多字节的 Git LFS pointer。
|
||||||
|
|
||||||
|
## 1. 服务器预检
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvidia-smi
|
||||||
|
docker compose -f docker-compose-gpu.yml config
|
||||||
|
bash server_gpu_test.sh preflight
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 构建并启动测试服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LLM_INFERENCE_BATCH_SIZE=4 bash server_gpu_test.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
对 24GB 显卡可将微批设为 `2`;48GB/80GB 显卡通常可使用 `4`。这个参数只控制 Qwen 内部微批,不会限制 API 一次提交的条数。
|
||||||
|
|
||||||
|
## 3. 单条与 32 条 LLM 冒烟测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash server_gpu_test.sh smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
另开一个终端观察峰值:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
watch -n 0.5 nvidia-smi
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:两个请求都返回 HTTP 200,日志中出现 `LLM inference micro-batching`,32 条请求不再把 32 条同时送入 Qwen。
|
||||||
|
|
||||||
|
## 4. 确认后上线
|
||||||
|
|
||||||
|
如果当前机器就是正式机,`start` 启动的已是 Compose 中的正式服务。检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose-gpu.yml ps
|
||||||
|
docker compose -f docker-compose-gpu.yml logs --tail=200 api
|
||||||
|
curl -fsS http://127.0.0.1:18000/
|
||||||
|
```
|
||||||
|
|
||||||
|
回滚时保留旧代码目录,然后在旧目录里重新执行 `docker compose up -d --build`。
|
||||||
@ -229,7 +229,12 @@ class BatchPredictRequest(BaseModel):
|
|||||||
items: List[PredictRequest] = Field(
|
items: List[PredictRequest] = Field(
|
||||||
..., min_length=1, max_length=500, description="配方列表,单次最多 500 条"
|
..., min_length=1, max_length=500, description="配方列表,单次最多 500 条"
|
||||||
)
|
)
|
||||||
batch_size: int = Field(default=32, ge=1, le=256, description="模型前向批大小")
|
batch_size: int = Field(
|
||||||
|
default=32,
|
||||||
|
ge=1,
|
||||||
|
le=256,
|
||||||
|
description="外层模型前向批大小;7B LLM 会按显存安全值内部微批",
|
||||||
|
)
|
||||||
use_llm: bool = Field(
|
use_llm: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="是否启用 LLM 分支;关闭可大幅提速,但会损失 delivery 精度",
|
description="是否启用 LLM 分支;关闭可大幅提速,但会损失 delivery 精度",
|
||||||
@ -668,4 +673,3 @@ if __name__ == "__main__":
|
|||||||
port=8000,
|
port=8000,
|
||||||
reload=True,
|
reload=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -571,7 +571,9 @@ def predict_all(
|
|||||||
all_ee_preds = []
|
all_ee_preds = []
|
||||||
all_toxic_preds = []
|
all_toxic_preds = []
|
||||||
|
|
||||||
with torch.no_grad():
|
# inference_mode 比 no_grad 还会关闭 version counter/view tracking,
|
||||||
|
# 对大批量 LLM 推理可再减少一部分临时内存。
|
||||||
|
with torch.inference_mode():
|
||||||
for batch in dataloader:
|
for batch in dataloader:
|
||||||
smiles = batch["smiles"]
|
smiles = batch["smiles"]
|
||||||
tabular = {k: v.to(device) for k, v in batch["tabular"].items()}
|
tabular = {k: v.to(device) for k, v in batch["tabular"].items()}
|
||||||
@ -1145,4 +1147,3 @@ def main(
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
app()
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,20 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: api
|
target: api
|
||||||
container_name: lnp-api
|
container_name: lnp-api
|
||||||
|
ports:
|
||||||
|
# 仅绑定本机,供服务器上的冒烟测试使用,不直接暴露到公网。
|
||||||
|
- "127.0.0.1:${API_PORT:-18000}:8000"
|
||||||
environment:
|
environment:
|
||||||
- MODEL_PATH=/app/models/final/model.pt
|
- MODEL_PATH=/app/models/final/model.pt
|
||||||
|
- RAG_POOL_CSV=/app/data/interim/internal.csv
|
||||||
|
# 7B LLM 内部微批;外层批量筛选仍可一次提交更多条。
|
||||||
|
- LLM_INFERENCE_BATCH_SIZE=${LLM_INFERENCE_BATCH_SIZE:-4}
|
||||||
volumes:
|
volumes:
|
||||||
# 挂载模型目录以便更新模型
|
# 权重和 RAG 数据不进镜像,服务器上需预先放到这些路径。
|
||||||
- ./models/final:/app/models/final:ro
|
- ./models/final:/app/models/final:ro
|
||||||
- ./models/mpnn:/app/models/mpnn:ro
|
- ./models/mpnn:/app/models/mpnn:ro
|
||||||
|
- ./models/qwen2.5-7b-instruct:/app/models/qwen2.5-7b-instruct:ro
|
||||||
|
- ./data/interim/internal.csv:/app/data/interim/internal.csv:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||||
|
|||||||
@ -9,6 +9,16 @@ import torch.nn as nn
|
|||||||
|
|
||||||
DEFAULT_MOLT5_PATH = os.environ.get("MOLT5_PATH", "models/molt5-base")
|
DEFAULT_MOLT5_PATH = os.environ.get("MOLT5_PATH", "models/molt5-base")
|
||||||
|
|
||||||
|
|
||||||
|
def _positive_int_env(name: str, default: int) -> int:
|
||||||
|
"""Read a positive integer environment variable with a safe fallback."""
|
||||||
|
try:
|
||||||
|
value = int(os.environ.get(name, str(default)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
return value if value > 0 else default
|
||||||
|
|
||||||
|
|
||||||
# 检索池支持的额外多任务(除 delivery 外)
|
# 检索池支持的额外多任务(除 delivery 外)
|
||||||
_EXTRA_TASKS = ["size", "pdi", "ee", "toxic", "biodist"]
|
_EXTRA_TASKS = ["size", "pdi", "ee", "toxic", "biodist"]
|
||||||
|
|
||||||
@ -58,6 +68,9 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
self.use_rag = use_rag
|
self.use_rag = use_rag
|
||||||
self.rag_top_k = rag_top_k
|
self.rag_top_k = rag_top_k
|
||||||
self.use_soft_prompt = use_soft_prompt
|
self.use_soft_prompt = use_soft_prompt
|
||||||
|
# 7B 模型即使在 no_grad 下,大批量长 prompt 的临时激活也很大。
|
||||||
|
# 外层 DataLoader 仍可用大 batch;这里只把 LLM 前向切成小块。
|
||||||
|
self.inference_batch_size = _positive_int_env("LLM_INFERENCE_BATCH_SIZE", 4)
|
||||||
|
|
||||||
_name_l = model_name_or_path.lower()
|
_name_l = model_name_or_path.lower()
|
||||||
_is_t5 = "t5" in _name_l
|
_is_t5 = "t5" in _name_l
|
||||||
@ -145,16 +158,18 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
def _apply_lora(self, r, alpha, dropout, prepare_kbit=False):
|
def _apply_lora(self, r, alpha, dropout, prepare_kbit=False):
|
||||||
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
|
||||||
|
|
||||||
if prepare_kbit:
|
# 4-bit/8-bit 基座只 prepare 一次。PEFT 会处理输入梯度并开启
|
||||||
|
# gradient checkpointing;后面再显式开一次,兼容不同 PEFT 版本。
|
||||||
|
is_kbit = (
|
||||||
|
prepare_kbit
|
||||||
|
or getattr(self.encoder, "is_loaded_in_4bit", False)
|
||||||
|
or getattr(self.encoder, "is_loaded_in_8bit", False)
|
||||||
|
)
|
||||||
|
if is_kbit:
|
||||||
self.encoder = prepare_model_for_kbit_training(self.encoder)
|
self.encoder = prepare_model_for_kbit_training(self.encoder)
|
||||||
else:
|
else:
|
||||||
for p in self.encoder.parameters():
|
for p in self.encoder.parameters():
|
||||||
p.requires_grad = False
|
p.requires_grad = False
|
||||||
|
|
||||||
# 4-bit 量化模型(QLoRA)需先 prepare,才能正确接收梯度
|
|
||||||
if getattr(self.encoder, "is_loaded_in_4bit", False) or getattr(self.encoder, "is_loaded_in_8bit", False):
|
|
||||||
from peft import prepare_model_for_kbit_training
|
|
||||||
self.encoder = prepare_model_for_kbit_training(self.encoder)
|
|
||||||
for p in self.encoder.parameters():
|
for p in self.encoder.parameters():
|
||||||
p.requires_grad = False
|
p.requires_grad = False
|
||||||
# 不同架构注意力层命名不同:
|
# 不同架构注意力层命名不同:
|
||||||
@ -170,6 +185,24 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
target_modules=_targets, bias="none")
|
target_modules=_targets, bias="none")
|
||||||
self.encoder = get_peft_model(self.encoder, cfg)
|
self.encoder = get_peft_model(self.encoder, cfg)
|
||||||
|
|
||||||
|
# Decoder-only LLM 做表征编码不需要 KV cache。训练时保留 cache
|
||||||
|
# 只会额外占显存;梯度检查点则用计算换显存。
|
||||||
|
if hasattr(self.encoder, "config"):
|
||||||
|
self.encoder.config.use_cache = False
|
||||||
|
if hasattr(self.encoder, "gradient_checkpointing_enable"):
|
||||||
|
try:
|
||||||
|
self.encoder.gradient_checkpointing_enable(
|
||||||
|
gradient_checkpointing_kwargs={"use_reentrant": False}
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
self.encoder.gradient_checkpointing_enable()
|
||||||
|
|
||||||
|
def _encoder_forward(self, **kwargs):
|
||||||
|
"""Forward the backbone without allocating an unused decoder KV cache."""
|
||||||
|
if self._is_qwen:
|
||||||
|
kwargs["use_cache"] = False
|
||||||
|
return self.encoder(**kwargs)
|
||||||
|
|
||||||
# ---------- 检索池 ----------
|
# ---------- 检索池 ----------
|
||||||
def set_retrieval_pool(self, smiles_list, labels, pool_id="train", extra_labels=None):
|
def set_retrieval_pool(self, smiles_list, labels, pool_id="train", extra_labels=None):
|
||||||
"""设置 RAG 检索池(防泄漏:只传训练集分子与标签)。
|
"""设置 RAG 检索池(防泄漏:只传训练集分子与标签)。
|
||||||
@ -326,14 +359,15 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
def _rag_encode_batch(self, prompts, device):
|
def _rag_encode_batch(self, prompts, device):
|
||||||
"""编码一批 RAG prompt,取最后有效 token。grad 由调用方上下文决定。"""
|
"""编码一批 RAG prompt,取最后有效 token。grad 由调用方上下文决定。"""
|
||||||
outs = []
|
outs = []
|
||||||
for i in range(0, len(prompts), 4):
|
chunk_size = self.inference_batch_size
|
||||||
bt = prompts[i:i+4]
|
for i in range(0, len(prompts), chunk_size):
|
||||||
|
bt = prompts[i:i + chunk_size]
|
||||||
enc = self.tokenizer(
|
enc = self.tokenizer(
|
||||||
bt, padding=True, truncation=True,
|
bt, padding=True, truncation=True,
|
||||||
max_length=self.max_length, return_tensors="pt",
|
max_length=self.max_length, return_tensors="pt",
|
||||||
).to(device)
|
).to(device)
|
||||||
self._warn_if_truncated(enc)
|
self._warn_if_truncated(enc)
|
||||||
out = self.encoder(**enc).last_hidden_state # [B,L,H]
|
out = self._encoder_forward(**enc).last_hidden_state # [B,L,H]
|
||||||
lengths = enc["attention_mask"].sum(1) - 1
|
lengths = enc["attention_mask"].sum(1) - 1
|
||||||
b = torch.arange(out.size(0), device=device)
|
b = torch.arange(out.size(0), device=device)
|
||||||
outs.append(out[b, lengths.long(), :].float()) # [B,H]
|
outs.append(out[b, lengths.long(), :].float()) # [B,H]
|
||||||
@ -359,7 +393,8 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
return self._rag_encode_batch(prompts, device)
|
return self._rag_encode_batch(prompts, device)
|
||||||
|
|
||||||
# ---------- soft-prompt 编码(带梯度,不缓存特征)----------
|
# ---------- soft-prompt 编码(带梯度,不缓存特征)----------
|
||||||
def _encode_softrag(self, smiles, chem, tab, device) -> torch.Tensor:
|
def _encode_softrag_chunk(self, smiles, chem, tab, device) -> torch.Tensor:
|
||||||
|
"""单个 LLM micro-batch 的 soft-RAG 前向。"""
|
||||||
prompts = [self._get_prompt(s) for s in smiles]
|
prompts = [self._get_prompt(s) for s in smiles]
|
||||||
enc = self.tokenizer(
|
enc = self.tokenizer(
|
||||||
prompts, padding=True, truncation=True,
|
prompts, padding=True, truncation=True,
|
||||||
@ -393,7 +428,7 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
position_ids = attn_mask.long().cumsum(-1) - 1
|
position_ids = attn_mask.long().cumsum(-1) - 1
|
||||||
position_ids = position_ids.masked_fill(attn_mask == 0, 1)
|
position_ids = position_ids.masked_fill(attn_mask == 0, 1)
|
||||||
|
|
||||||
out = self.encoder(
|
out = self._encoder_forward(
|
||||||
inputs_embeds=inputs_embeds,
|
inputs_embeds=inputs_embeds,
|
||||||
attention_mask=attn_mask,
|
attention_mask=attn_mask,
|
||||||
position_ids=position_ids,
|
position_ids=position_ids,
|
||||||
@ -407,6 +442,43 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
feat = out[b, lengths.long(), :]
|
feat = out[b, lengths.long(), :]
|
||||||
return self.proj_down(feat.float())
|
return self.proj_down(feat.float())
|
||||||
|
|
||||||
|
def _encode_softrag(self, smiles, chem, tab, device) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
soft-RAG 编码。推理时自动微批,避免 batch API 把 7B LLM 的
|
||||||
|
临时激活按请求 batch_size 线性放大。训练仍保持原 batch,
|
||||||
|
由 gradient checkpointing 降低激活显存。
|
||||||
|
"""
|
||||||
|
batch_size = len(smiles)
|
||||||
|
micro_batch = self.inference_batch_size
|
||||||
|
should_chunk = (
|
||||||
|
not self.training
|
||||||
|
and not torch.is_grad_enabled()
|
||||||
|
and batch_size > micro_batch
|
||||||
|
)
|
||||||
|
if not should_chunk:
|
||||||
|
return self._encode_softrag_chunk(smiles, chem, tab, device)
|
||||||
|
|
||||||
|
if not getattr(self, "_microbatch_logged", False):
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"LLM inference micro-batching: batch={batch_size}, "
|
||||||
|
f"micro_batch={micro_batch} (override with LLM_INFERENCE_BATCH_SIZE)"
|
||||||
|
)
|
||||||
|
self._microbatch_logged = True
|
||||||
|
|
||||||
|
outputs = []
|
||||||
|
for start in range(0, batch_size, micro_batch):
|
||||||
|
end = min(start + micro_batch, batch_size)
|
||||||
|
chem_chunk = chem[start:end] if chem is not None else None
|
||||||
|
tab_chunk = tab[start:end] if tab is not None else None
|
||||||
|
outputs.append(
|
||||||
|
self._encode_softrag_chunk(
|
||||||
|
smiles[start:end], chem_chunk, tab_chunk, device
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return torch.cat(outputs, dim=0)
|
||||||
|
|
||||||
# ---------- 旧路径(保留,向后兼容)----------
|
# ---------- 旧路径(保留,向后兼容)----------
|
||||||
def _mean_pool(self, last_hidden, mask):
|
def _mean_pool(self, last_hidden, mask):
|
||||||
m = mask.unsqueeze(-1).float()
|
m = mask.unsqueeze(-1).float()
|
||||||
@ -417,12 +489,13 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
missing = [s for s in smiles if s not in self._cache]
|
missing = [s for s in smiles if s not in self._cache]
|
||||||
if missing:
|
if missing:
|
||||||
uniq = list(dict.fromkeys(missing))
|
uniq = list(dict.fromkeys(missing))
|
||||||
for i in range(0, len(uniq), 256):
|
chunk_size = self.inference_batch_size if self._is_qwen else 256
|
||||||
chunk = uniq[i:i + 256]
|
for i in range(0, len(uniq), chunk_size):
|
||||||
|
chunk = uniq[i:i + chunk_size]
|
||||||
mols = [self._fmt_mol(s) for s in chunk]
|
mols = [self._fmt_mol(s) for s in chunk]
|
||||||
enc = self.tokenizer(mols, padding=True, truncation=True,
|
enc = self.tokenizer(mols, padding=True, truncation=True,
|
||||||
max_length=self.max_length, return_tensors="pt").to(device)
|
max_length=self.max_length, return_tensors="pt").to(device)
|
||||||
out = self.encoder(**enc).last_hidden_state
|
out = self._encoder_forward(**enc).last_hidden_state
|
||||||
pooled = self._mean_pool(out, enc["attention_mask"])
|
pooled = self._mean_pool(out, enc["attention_mask"])
|
||||||
for s, v in zip(chunk, pooled):
|
for s, v in zip(chunk, pooled):
|
||||||
self._cache[s] = v.cpu()
|
self._cache[s] = v.cpu()
|
||||||
@ -432,7 +505,7 @@ class LLMPromptEncoder(nn.Module):
|
|||||||
mols = [self._fmt_mol(s) for s in smiles]
|
mols = [self._fmt_mol(s) for s in smiles]
|
||||||
enc = self.tokenizer(mols, padding=True, truncation=True,
|
enc = self.tokenizer(mols, padding=True, truncation=True,
|
||||||
max_length=self.max_length, return_tensors="pt").to(device)
|
max_length=self.max_length, return_tensors="pt").to(device)
|
||||||
out = self.encoder(**enc).last_hidden_state
|
out = self._encoder_forward(**enc).last_hidden_state
|
||||||
return self._mean_pool(out, enc["attention_mask"])
|
return self._mean_pool(out, enc["attention_mask"])
|
||||||
|
|
||||||
def forward(self, smiles: List[str], chem: Optional[torch.Tensor] = None,
|
def forward(self, smiles: List[str], chem: Optional[torch.Tensor] = None,
|
||||||
|
|||||||
@ -118,7 +118,7 @@ def load_model(
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.inference_mode()
|
||||||
def predict_batch(
|
def predict_batch(
|
||||||
model: Union[LNPModel, LNPModelWithoutMPNN],
|
model: Union[LNPModel, LNPModelWithoutMPNN],
|
||||||
loader: DataLoader,
|
loader: DataLoader,
|
||||||
|
|||||||
@ -31,5 +31,6 @@ uvicorn = ">=0.33.0, <0.34"
|
|||||||
optuna = ">=4.5.0, <5"
|
optuna = ">=4.5.0, <5"
|
||||||
captum = ">=0.7.0, <0.8"
|
captum = ">=0.7.0, <0.8"
|
||||||
transformers = ">=4.30, <4.46"
|
transformers = ">=4.30, <4.46"
|
||||||
|
peft = "==0.13.2"
|
||||||
sentencepiece = "*"
|
sentencepiece = "*"
|
||||||
protobuf = "*"
|
protobuf = "*"
|
||||||
|
|||||||
@ -21,6 +21,7 @@ uvicorn>=0.33.0,<0.34
|
|||||||
optuna>=4.5.0,<5
|
optuna>=4.5.0,<5
|
||||||
captum>=0.7.0
|
captum>=0.7.0
|
||||||
transformers>=4.30,<4.46
|
transformers>=4.30,<4.46
|
||||||
|
peft==0.13.2
|
||||||
sentencepiece
|
sentencepiece
|
||||||
protobuf
|
protobuf
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
|
|||||||
108
server_gpu_test.sh
Executable file
108
server_gpu_test.sh
Executable file
@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
cd "${ROOT_DIR}"
|
||||||
|
|
||||||
|
COMPOSE_FILE=${COMPOSE_FILE:-docker-compose-gpu.yml}
|
||||||
|
API_PORT=${API_PORT:-18000}
|
||||||
|
API_URL=${API_URL:-http://127.0.0.1:${API_PORT}}
|
||||||
|
LLM_INFERENCE_BATCH_SIZE=${LLM_INFERENCE_BATCH_SIZE:-4}
|
||||||
|
export API_PORT
|
||||||
|
export LLM_INFERENCE_BATCH_SIZE
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_real_file() {
|
||||||
|
local path=$1
|
||||||
|
[ -f "${path}" ] || fail "missing file: ${path}"
|
||||||
|
if head -n 1 "${path}" 2>/dev/null | grep -q "git-lfs.github.com/spec"; then
|
||||||
|
fail "${path} is a Git LFS pointer, not the real artifact"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
preflight() {
|
||||||
|
command -v docker >/dev/null || fail "docker is not installed"
|
||||||
|
command -v nvidia-smi >/dev/null || fail "nvidia-smi is not installed"
|
||||||
|
nvidia-smi >/dev/null || fail "NVIDIA driver/GPU is unavailable"
|
||||||
|
docker compose version >/dev/null || fail "docker compose plugin is unavailable"
|
||||||
|
|
||||||
|
assert_real_file models/final/model.pt
|
||||||
|
assert_real_file data/interim/internal.csv
|
||||||
|
assert_real_file models/qwen2.5-7b-instruct/config.json
|
||||||
|
|
||||||
|
local fold
|
||||||
|
for fold in 0 1 2 3 4; do
|
||||||
|
assert_real_file "models/mpnn/all_amine_split_for_LiON/cv_${fold}/fold_0/model_0/model.pt"
|
||||||
|
done
|
||||||
|
|
||||||
|
find models/qwen2.5-7b-instruct -maxdepth 1 -type f \
|
||||||
|
\( -name '*.safetensors' -o -name '*.bin' \) -print -quit | grep -q . \
|
||||||
|
|| fail "Qwen weight shards (*.safetensors or *.bin) are missing"
|
||||||
|
|
||||||
|
docker compose -f "${COMPOSE_FILE}" config >/dev/null
|
||||||
|
echo "preflight: OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_api() {
|
||||||
|
local attempt
|
||||||
|
for attempt in $(seq 1 120); do
|
||||||
|
if curl -fsS "${API_URL}/" >/dev/null 2>&1; then
|
||||||
|
echo "API ready after ${attempt} checks"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
docker compose -f "${COMPOSE_FILE}" logs --tail=200 api || true
|
||||||
|
fail "API did not become healthy within 240 seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
preflight
|
||||||
|
docker compose -f "${COMPOSE_FILE}" build
|
||||||
|
docker compose -f "${COMPOSE_FILE}" up -d
|
||||||
|
wait_for_api
|
||||||
|
docker compose -f "${COMPOSE_FILE}" ps
|
||||||
|
curl -fsS "${API_URL}/"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
request_item() {
|
||||||
|
printf '%s' '{"smiles":"CC(C)NCCNC(C)C","cationic_lipid_to_mrna_ratio":10.0,"cationic_lipid_mol_ratio":35.0,"phospholipid_mol_ratio":16.0,"cholesterol_mol_ratio":46.5,"peg_lipid_mol_ratio":2.5,"helper_lipid":"DOPE","route":"intravenous"}'
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke() {
|
||||||
|
wait_for_api
|
||||||
|
|
||||||
|
local item payload i
|
||||||
|
item=$(request_item)
|
||||||
|
curl -fsS -H 'Content-Type: application/json' \
|
||||||
|
-d "${item}" "${API_URL}/predict" >/tmp/lnp-single-response.json
|
||||||
|
echo "single prediction: OK"
|
||||||
|
|
||||||
|
payload='{"items":['
|
||||||
|
for i in $(seq 1 32); do
|
||||||
|
[ "${i}" -eq 1 ] || payload+=','
|
||||||
|
payload+="${item}"
|
||||||
|
done
|
||||||
|
payload+='],"batch_size":32,"use_llm":true}'
|
||||||
|
|
||||||
|
curl -fsS -H 'Content-Type: application/json' \
|
||||||
|
-d "${payload}" "${API_URL}/predict/batch" >/tmp/lnp-batch-response.json
|
||||||
|
grep -q '"n_succeeded":32' /tmp/lnp-batch-response.json \
|
||||||
|
|| fail "batch response did not report 32 successes"
|
||||||
|
echo "32-item LLM prediction: OK"
|
||||||
|
|
||||||
|
docker compose -f "${COMPOSE_FILE}" logs --tail=200 api \
|
||||||
|
| grep -E 'LLM inference micro-batching|ERROR|CUDA out of memory' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
preflight) preflight ;;
|
||||||
|
start) start ;;
|
||||||
|
smoke) smoke ;;
|
||||||
|
*) echo "Usage: $0 {preflight|start|smoke}" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
88
server_test_cases/README.md
Normal file
88
server_test_cases/README.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# LNP API 与 GPU 显存测试
|
||||||
|
|
||||||
|
这里有两套测试:
|
||||||
|
|
||||||
|
- `run_lnp_batch_stress.sh`:专门复现 LNP 批量调用的显存问题。
|
||||||
|
- `run_cases.sh`:检查健康状态、器官列表、单条预测、批量预测、异常输入和小规模配方优化。
|
||||||
|
|
||||||
|
## 1. 复现 LNP 批量显存场景
|
||||||
|
|
||||||
|
推荐先运行与问题最接近的场景:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash server_test_cases/run_lnp_batch_stress.sh repro
|
||||||
|
```
|
||||||
|
|
||||||
|
`repro` 会依次提交:
|
||||||
|
|
||||||
|
1. 32 条不同 LNP 配方,`use_llm=true`、外层 `batch_size=32`。这是原先最容易从约 10GB 涨到 63GB 的场景。
|
||||||
|
2. 500 条不同 LNP 配方,仍使用外层 `batch_size=32`,检查多轮处理后是否持续涨显存。
|
||||||
|
|
||||||
|
逐级观察批量大小与显存的关系:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash server_test_cases/run_lnp_batch_stress.sh ladder
|
||||||
|
```
|
||||||
|
|
||||||
|
完整测试(关闭 LLM 对照 + 阶梯 + 500 条持续压力):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash server_test_cases/run_lnp_batch_stress.sh full
|
||||||
|
```
|
||||||
|
|
||||||
|
每个场景都会打印并写入 CSV:运行前显存、采样峰值、显存增量、耗时、HTTP 状态。结果文件名形如 `vram-results-20260922-160000.csv`。
|
||||||
|
|
||||||
|
可以设置显存上限,让超过阈值时测试直接失败。例如期望不超过 20GB:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MAX_GPU_MIB=20480 bash server_test_cases/run_lnp_batch_stress.sh repro
|
||||||
|
```
|
||||||
|
|
||||||
|
这里采样的是整张卡的显存;测试时请尽量不要让其他任务共用该卡。
|
||||||
|
|
||||||
|
## 2. Mock 数据
|
||||||
|
|
||||||
|
已附带三组可直接提交的数据:
|
||||||
|
|
||||||
|
- `mock_batch_32_llm.json`:核心显存回归场景。
|
||||||
|
- `mock_batch_500_llm.json`:最大请求量与持续处理场景。
|
||||||
|
- `mock_batch_128_no_llm.json`:关闭 LLM 的对照组。
|
||||||
|
|
||||||
|
这些数据从项目 `data/interim/internal.csv` 抽取不同 SMILES,再随机生成满足总和 100% 的组分比例。也可以自行生成:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 server_test_cases/generate_mock_batch.py \
|
||||||
|
--count 200 \
|
||||||
|
--batch-size 32 \
|
||||||
|
--use-llm \
|
||||||
|
--source-csv data/interim/internal.csv \
|
||||||
|
--output /tmp/lnp-batch-200.json
|
||||||
|
```
|
||||||
|
|
||||||
|
生成过程使用固定随机种子,便于前后版本用完全相同的数据对比。
|
||||||
|
|
||||||
|
## 3. 其他功能测试
|
||||||
|
|
||||||
|
服务启动后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash server_test_cases/run_cases.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 API 端口或 GPU 编号不同:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
API_PORT=18000 GPU_INDEX=0 bash server_test_cases/run_cases.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
用例包含:
|
||||||
|
|
||||||
|
1. 健康检查与模型状态。
|
||||||
|
2. 可用器官列表。
|
||||||
|
3. 启用 LLM 的单条预测,检查输出完整性、有限数和 biodistribution 归一化。
|
||||||
|
4. 64 条关闭 LLM 的吞吐对照。
|
||||||
|
5. 32 条启用 LLM 的显存回归测试。
|
||||||
|
6. 2 条合法 + 3 条非法输入,检查批量接口的局部容错。
|
||||||
|
7. 小范围 `/optimize` 配方搜索,检查优化功能和返回结果。
|
||||||
|
|
||||||
|
GPU 显存采样来自整张卡;如果还有其他进程使用同一张卡,请以 `nvidia-smi` 中 API 进程的变化为准。
|
||||||
102
server_test_cases/generate_mock_batch.py
Executable file
102
server_test_cases/generate_mock_batch.py
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate reproducible /predict/batch payloads for GPU memory tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# Used only when data/interim/internal.csv is not available. These are valid,
|
||||||
|
# deliberately varied SMILES; the project dataset is preferred for realistic RAG prompts.
|
||||||
|
FALLBACK_SMILES = [
|
||||||
|
"CC(C)NCCNC(C)C",
|
||||||
|
"CCCCN(CC)CC",
|
||||||
|
"CCCCCCCCN(C)C",
|
||||||
|
"CCN(CC)CCOC(=O)CCCC",
|
||||||
|
"CCCCCCCCCCN(CC)CC",
|
||||||
|
"CC(C)(C)OC(=O)NCCN(C)C",
|
||||||
|
"CCCCCCCCCCCCN(C)C",
|
||||||
|
"CCOC(=O)CN(C)C",
|
||||||
|
"CCCCCCN(CC)CC",
|
||||||
|
"CN(C)CCOC(=O)CCCCCCCC",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_smiles(source: Path | None) -> list[str]:
|
||||||
|
if source and source.is_file():
|
||||||
|
with source.open(encoding="utf-8-sig", newline="") as handle:
|
||||||
|
rows = csv.DictReader(handle)
|
||||||
|
values = []
|
||||||
|
seen = set()
|
||||||
|
for row in rows:
|
||||||
|
value = (row.get("smiles") or "").strip()
|
||||||
|
if value and value.lower() != "nan" and value not in seen:
|
||||||
|
seen.add(value)
|
||||||
|
values.append(value)
|
||||||
|
if values:
|
||||||
|
return values
|
||||||
|
return FALLBACK_SMILES
|
||||||
|
|
||||||
|
|
||||||
|
def make_composition(rng: random.Random) -> tuple[float, float, float, float]:
|
||||||
|
# Match the ranges used by the optimizer and guarantee an exact 100% sum.
|
||||||
|
for _ in range(1000):
|
||||||
|
peg = round(rng.uniform(1.0, 5.0), 2)
|
||||||
|
cationic = round(rng.uniform(25.0, 53.0), 2)
|
||||||
|
phospholipid = round(rng.uniform(8.0, 30.0), 2)
|
||||||
|
cholesterol = round(100.0 - peg - cationic - phospholipid, 2)
|
||||||
|
if 15.0 <= cholesterol <= 46.0:
|
||||||
|
return cationic, phospholipid, cholesterol, peg
|
||||||
|
raise RuntimeError("unable to generate a valid composition")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--count", type=int, required=True, choices=range(1, 501))
|
||||||
|
parser.add_argument("--batch-size", type=int, default=32, choices=range(1, 257))
|
||||||
|
parser.add_argument("--use-llm", action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("--seed", type=int, default=20260922)
|
||||||
|
parser.add_argument("--source-csv", type=Path)
|
||||||
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
rng = random.Random(args.seed)
|
||||||
|
smiles_pool = load_smiles(args.source_csv)
|
||||||
|
rng.shuffle(smiles_pool)
|
||||||
|
items = []
|
||||||
|
for index in range(args.count):
|
||||||
|
cationic, phospholipid, cholesterol, peg = make_composition(rng)
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"smiles": smiles_pool[index % len(smiles_pool)],
|
||||||
|
"cationic_lipid_to_mrna_ratio": round(rng.uniform(7.0, 20.0), 2),
|
||||||
|
"cationic_lipid_mol_ratio": cationic,
|
||||||
|
"phospholipid_mol_ratio": phospholipid,
|
||||||
|
"cholesterol_mol_ratio": cholesterol,
|
||||||
|
"peg_lipid_mol_ratio": peg,
|
||||||
|
"helper_lipid": ("DOPE", "DSPC")[index % 2],
|
||||||
|
"route": ("intravenous", "intramuscular")[index % 2],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"items": items,
|
||||||
|
"batch_size": args.batch_size,
|
||||||
|
"use_llm": args.use_llm,
|
||||||
|
}
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(
|
||||||
|
json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"generated {args.output}: items={args.count}, unique_smiles={len(set(x['smiles'] for x in items))}, "
|
||||||
|
f"batch_size={args.batch_size}, use_llm={args.use_llm}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
56
server_test_cases/mixed_valid_invalid.json
Normal file
56
server_test_cases/mixed_valid_invalid.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"smiles": "CC(C)NCCNC(C)C",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.0,
|
||||||
|
"cationic_lipid_mol_ratio": 35.0,
|
||||||
|
"phospholipid_mol_ratio": 16.0,
|
||||||
|
"cholesterol_mol_ratio": 46.5,
|
||||||
|
"peg_lipid_mol_ratio": 2.5,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCCCCCCCCCCCCCCCN",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 12.0,
|
||||||
|
"cationic_lipid_mol_ratio": 40.0,
|
||||||
|
"phospholipid_mol_ratio": 15.0,
|
||||||
|
"cholesterol_mol_ratio": 42.5,
|
||||||
|
"peg_lipid_mol_ratio": 2.5,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "this-is-not-smiles",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.0,
|
||||||
|
"cationic_lipid_mol_ratio": 35.0,
|
||||||
|
"phospholipid_mol_ratio": 16.0,
|
||||||
|
"cholesterol_mol_ratio": 46.5,
|
||||||
|
"peg_lipid_mol_ratio": 2.5,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCN(CC)CC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.0,
|
||||||
|
"cationic_lipid_mol_ratio": 35.0,
|
||||||
|
"phospholipid_mol_ratio": 16.0,
|
||||||
|
"cholesterol_mol_ratio": 30.0,
|
||||||
|
"peg_lipid_mol_ratio": 2.5,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "C1CCN(CC1)CCO",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.0,
|
||||||
|
"cationic_lipid_mol_ratio": 35.0,
|
||||||
|
"phospholipid_mol_ratio": 16.0,
|
||||||
|
"cholesterol_mol_ratio": 46.5,
|
||||||
|
"peg_lipid_mol_ratio": 2.5,
|
||||||
|
"helper_lipid": "INVALID_HELPER",
|
||||||
|
"route": "intravenous"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batch_size": 5,
|
||||||
|
"use_llm": false
|
||||||
|
}
|
||||||
1286
server_test_cases/mock_batch_128_no_llm.json
Normal file
1286
server_test_cases/mock_batch_128_no_llm.json
Normal file
File diff suppressed because it is too large
Load Diff
326
server_test_cases/mock_batch_32_llm.json
Normal file
326
server_test_cases/mock_batch_32_llm.json
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O)CCNC(C[C@@H](CN)CC(C)C)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 7.46,
|
||||||
|
"cationic_lipid_mol_ratio": 37.58,
|
||||||
|
"phospholipid_mol_ratio": 23.5,
|
||||||
|
"cholesterol_mol_ratio": 36.53,
|
||||||
|
"peg_lipid_mol_ratio": 2.39,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCCCC)=O)CCNC(CCC(NCC1=CC=C(B(O)O)C=C1)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 16.82,
|
||||||
|
"cationic_lipid_mol_ratio": 50.09,
|
||||||
|
"phospholipid_mol_ratio": 13.01,
|
||||||
|
"cholesterol_mol_ratio": 33.14,
|
||||||
|
"peg_lipid_mol_ratio": 3.76,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCN([C@H](C(NCCCN(CCCCCCOC(CCCCCCC)=O)CCCCCCOC(CCCCCCC)=O)=O)CO)CCCCCCOC(CCCCCCC)=O)CCCCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 9.92,
|
||||||
|
"cationic_lipid_mol_ratio": 46.82,
|
||||||
|
"phospholipid_mol_ratio": 16.37,
|
||||||
|
"cholesterol_mol_ratio": 33.48,
|
||||||
|
"peg_lipid_mol_ratio": 3.33,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCN([C@H](C(NCCCCCCN(CCCCCCOC(CCCCC)=O)CCCCCCOC(CCCCC)=O)=O)CO)CCCCCCOC(CCCCC)=O)CCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.73,
|
||||||
|
"cationic_lipid_mol_ratio": 36.31,
|
||||||
|
"phospholipid_mol_ratio": 23.42,
|
||||||
|
"cholesterol_mol_ratio": 35.49,
|
||||||
|
"peg_lipid_mol_ratio": 4.78,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN(CC1=NC=CS1)CC2=NC=CS2)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 19.35,
|
||||||
|
"cationic_lipid_mol_ratio": 51.39,
|
||||||
|
"phospholipid_mol_ratio": 10.87,
|
||||||
|
"cholesterol_mol_ratio": 35.21,
|
||||||
|
"peg_lipid_mol_ratio": 2.53,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCCCC)=O)CCCCCCN(CCCCCCN(CCC(OCCCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCCCC)=O)CCCN1CCOCC1",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 17.21,
|
||||||
|
"cationic_lipid_mol_ratio": 46.17,
|
||||||
|
"phospholipid_mol_ratio": 19.64,
|
||||||
|
"cholesterol_mol_ratio": 31.96,
|
||||||
|
"peg_lipid_mol_ratio": 2.23,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCCCCCCCCCCC(OCCCCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCOC([C@@H](N(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CO)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.34,
|
||||||
|
"cationic_lipid_mol_ratio": 44.28,
|
||||||
|
"phospholipid_mol_ratio": 17.74,
|
||||||
|
"cholesterol_mol_ratio": 34.79,
|
||||||
|
"peg_lipid_mol_ratio": 3.19,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCN([C@H](C(NCCN(C)CCN(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)=O)CO)CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCCCC=C",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 9.15,
|
||||||
|
"cationic_lipid_mol_ratio": 42.88,
|
||||||
|
"phospholipid_mol_ratio": 10.3,
|
||||||
|
"cholesterol_mol_ratio": 43.76,
|
||||||
|
"peg_lipid_mol_ratio": 3.06,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CC(CNC([C@@H](N(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)CO)=O)(CN(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)C",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.12,
|
||||||
|
"cationic_lipid_mol_ratio": 49.74,
|
||||||
|
"phospholipid_mol_ratio": 12.42,
|
||||||
|
"cholesterol_mol_ratio": 35.63,
|
||||||
|
"peg_lipid_mol_ratio": 2.21,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(NCCN(CCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CCCN",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 19.72,
|
||||||
|
"cationic_lipid_mol_ratio": 32.8,
|
||||||
|
"phospholipid_mol_ratio": 25.26,
|
||||||
|
"cholesterol_mol_ratio": 38.21,
|
||||||
|
"peg_lipid_mol_ratio": 3.73,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN1CC(C(OC)=O)CC1=O)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 13.7,
|
||||||
|
"cationic_lipid_mol_ratio": 43.91,
|
||||||
|
"phospholipid_mol_ratio": 21.35,
|
||||||
|
"cholesterol_mol_ratio": 31.92,
|
||||||
|
"peg_lipid_mol_ratio": 2.82,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCCCCCCCCCCCOC(CCN([C@H](C(NCCCCCCN(CCC(OCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCC)=O)=O)CO)CCC(OCCCCCCCCCCCC)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.47,
|
||||||
|
"cationic_lipid_mol_ratio": 32.49,
|
||||||
|
"phospholipid_mol_ratio": 24.48,
|
||||||
|
"cholesterol_mol_ratio": 40.96,
|
||||||
|
"peg_lipid_mol_ratio": 2.07,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C([C@H](CO)N(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)N(CC1)CCN1C2=NC=CC=N2",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 12.86,
|
||||||
|
"cationic_lipid_mol_ratio": 51.8,
|
||||||
|
"phospholipid_mol_ratio": 19.97,
|
||||||
|
"cholesterol_mol_ratio": 25.07,
|
||||||
|
"peg_lipid_mol_ratio": 3.16,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(CN(CC(O)CCCCCCCC)CC(O)CCCCCCCC)NCCCN(CC(O)CCCCCCCC)CC(O)CCCCCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 18.73,
|
||||||
|
"cationic_lipid_mol_ratio": 45.45,
|
||||||
|
"phospholipid_mol_ratio": 13.35,
|
||||||
|
"cholesterol_mol_ratio": 38.13,
|
||||||
|
"peg_lipid_mol_ratio": 3.07,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCCCCCCCCCCC(OCCCCCCN(CCN(C)CCNC([C@@H](N(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CO)=O)CCCCCCOC(CCCCCCCCCCC)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.09,
|
||||||
|
"cationic_lipid_mol_ratio": 37.43,
|
||||||
|
"phospholipid_mol_ratio": 11.94,
|
||||||
|
"cholesterol_mol_ratio": 45.78,
|
||||||
|
"peg_lipid_mol_ratio": 4.85,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "OC[C@@H](C(NCCCCCCN(CC(O)CCCCCCCCCC)CC(O)CCCCCCCCCC)=O)N(CC(CCCCCCCCCC)O)CC(CCCCCCCCCC)O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 13.05,
|
||||||
|
"cationic_lipid_mol_ratio": 52.84,
|
||||||
|
"phospholipid_mol_ratio": 8.6,
|
||||||
|
"cholesterol_mol_ratio": 37.06,
|
||||||
|
"peg_lipid_mol_ratio": 1.5,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCCCC)=O)CCNC(C[C@@H](CN)CC(C)C)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 11.09,
|
||||||
|
"cationic_lipid_mol_ratio": 48.25,
|
||||||
|
"phospholipid_mol_ratio": 14.52,
|
||||||
|
"cholesterol_mol_ratio": 32.66,
|
||||||
|
"peg_lipid_mol_ratio": 4.57,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC/C=C\\C/C=C\\CCCCC)CCN(CCC(OCCCCCCCC/C=C\\C/C=C\\CCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC/C=C\\C/C=C\\CCCCC)=O)CCC(OCCCCCCCC/C=C\\C/C=C\\CCCCC)=O)CCNC(CCC(NCCC1=CC=C(O)C(O)=C1)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 19.63,
|
||||||
|
"cationic_lipid_mol_ratio": 31.98,
|
||||||
|
"phospholipid_mol_ratio": 25.5,
|
||||||
|
"cholesterol_mol_ratio": 40.57,
|
||||||
|
"peg_lipid_mol_ratio": 1.95,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(NCCCCCCN(CCCCCCCCCCCC)CCCCCCCCCCCC)[C@H](C)N(CCCCCCCCCCCC)CCCCCCCCCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 16.96,
|
||||||
|
"cationic_lipid_mol_ratio": 28.21,
|
||||||
|
"phospholipid_mol_ratio": 28.57,
|
||||||
|
"cholesterol_mol_ratio": 39.51,
|
||||||
|
"peg_lipid_mol_ratio": 3.71,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN(CCC(F)(F)F)CCC(F)(F)F)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 19.73,
|
||||||
|
"cationic_lipid_mol_ratio": 37.62,
|
||||||
|
"phospholipid_mol_ratio": 22.08,
|
||||||
|
"cholesterol_mol_ratio": 36.42,
|
||||||
|
"peg_lipid_mol_ratio": 3.88,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCCCCCCCC(CN(CCCNC([C@@H](N(CC(CCCCCCCC)O)CC(CCCCCCCC)O)CO)=O)CC(CCCCCCCC)O)O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 15.62,
|
||||||
|
"cationic_lipid_mol_ratio": 51.29,
|
||||||
|
"phospholipid_mol_ratio": 27.02,
|
||||||
|
"cholesterol_mol_ratio": 18.83,
|
||||||
|
"peg_lipid_mol_ratio": 2.86,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C([C@H](CO)N(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)N(CC1)CCN1C2=NC=CC=N2",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 14.57,
|
||||||
|
"cationic_lipid_mol_ratio": 38.85,
|
||||||
|
"phospholipid_mol_ratio": 18.16,
|
||||||
|
"cholesterol_mol_ratio": 40.17,
|
||||||
|
"peg_lipid_mol_ratio": 2.82,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "CCCCCCCCCCCC(OCCCCCCN([C@H](C(NCCCCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)=O)CO)CCCCCCOC(CCCCCCCCCCC)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 17.0,
|
||||||
|
"cationic_lipid_mol_ratio": 41.61,
|
||||||
|
"phospholipid_mol_ratio": 21.12,
|
||||||
|
"cholesterol_mol_ratio": 35.9,
|
||||||
|
"peg_lipid_mol_ratio": 1.37,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCC)=O)CCCCCCN(CCCCCCN(CCC(OCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCC)=O)CCN1CCOCC1",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 12.75,
|
||||||
|
"cationic_lipid_mol_ratio": 31.73,
|
||||||
|
"phospholipid_mol_ratio": 29.19,
|
||||||
|
"cholesterol_mol_ratio": 37.55,
|
||||||
|
"peg_lipid_mol_ratio": 1.53,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(CN(CCCCCCCCCCCCCC)CCCCCCCCCCCCCC)NCCCN(CCCCCCCCCCCCCC)CCCCCCCCCCCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 19.09,
|
||||||
|
"cationic_lipid_mol_ratio": 41.63,
|
||||||
|
"phospholipid_mol_ratio": 16.23,
|
||||||
|
"cholesterol_mol_ratio": 39.43,
|
||||||
|
"peg_lipid_mol_ratio": 2.71,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCCCN(CCCCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O)CCNC(CCC(NCCC1=CC=C(O)C(O)=C1)=O)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 16.96,
|
||||||
|
"cationic_lipid_mol_ratio": 34.46,
|
||||||
|
"phospholipid_mol_ratio": 28.65,
|
||||||
|
"cholesterol_mol_ratio": 32.06,
|
||||||
|
"peg_lipid_mol_ratio": 4.83,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O)CCNC([C@@H](N)CCCCN)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 14.76,
|
||||||
|
"cationic_lipid_mol_ratio": 34.58,
|
||||||
|
"phospholipid_mol_ratio": 25.73,
|
||||||
|
"cholesterol_mol_ratio": 35.03,
|
||||||
|
"peg_lipid_mol_ratio": 4.66,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(CN(CCCCCCCCCCCC)CCCCCCCCCCCC)NCCCN(CCCCCCCCCCCC)CCCCCCCCCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 7.01,
|
||||||
|
"cationic_lipid_mol_ratio": 42.86,
|
||||||
|
"phospholipid_mol_ratio": 23.71,
|
||||||
|
"cholesterol_mol_ratio": 30.73,
|
||||||
|
"peg_lipid_mol_ratio": 2.7,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN(CC1=COC=N1)CC2=COC=N2)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 7.11,
|
||||||
|
"cationic_lipid_mol_ratio": 43.3,
|
||||||
|
"phospholipid_mol_ratio": 22.32,
|
||||||
|
"cholesterol_mol_ratio": 31.55,
|
||||||
|
"peg_lipid_mol_ratio": 2.83,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCNC(CCCN)=O)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 18.85,
|
||||||
|
"cationic_lipid_mol_ratio": 45.22,
|
||||||
|
"phospholipid_mol_ratio": 17.28,
|
||||||
|
"cholesterol_mol_ratio": 33.01,
|
||||||
|
"peg_lipid_mol_ratio": 4.49,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(CN(CCCCCCCCCC)CCCCCCCCCC)NCCCN(CCCCCCCCCC)CCCCCCCCCC",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.74,
|
||||||
|
"cationic_lipid_mol_ratio": 38.47,
|
||||||
|
"phospholipid_mol_ratio": 25.72,
|
||||||
|
"cholesterol_mol_ratio": 32.81,
|
||||||
|
"peg_lipid_mol_ratio": 3.0,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"smiles": "O=C(OCCCCCCCC/C=C\\C/C=C\\CCCCC)CCN(CCC(OCCCCCCCC/C=C\\C/C=C\\CCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC/C=C\\C/C=C\\CCCCC)=O)CCC(OCCCCCCCC/C=C\\C/C=C\\CCCCC)=O)CCNC(CCCN)=O",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 17.44,
|
||||||
|
"cationic_lipid_mol_ratio": 42.07,
|
||||||
|
"phospholipid_mol_ratio": 17.36,
|
||||||
|
"cholesterol_mol_ratio": 36.89,
|
||||||
|
"peg_lipid_mol_ratio": 3.68,
|
||||||
|
"helper_lipid": "DSPC",
|
||||||
|
"route": "intramuscular"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batch_size": 32,
|
||||||
|
"use_llm": true
|
||||||
|
}
|
||||||
5006
server_test_cases/mock_batch_500_llm.json
Normal file
5006
server_test_cases/mock_batch_500_llm.json
Normal file
File diff suppressed because it is too large
Load Diff
31
server_test_cases/optimize_smoke.json
Normal file
31
server_test_cases/optimize_smoke.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"smiles": "CC(C)NCCNC(C)C",
|
||||||
|
"organ": "liver",
|
||||||
|
"top_k": 3,
|
||||||
|
"num_seeds": 5,
|
||||||
|
"top_per_seed": 1,
|
||||||
|
"rerank_top_n": 8,
|
||||||
|
"step_sizes": [5],
|
||||||
|
"wr_step_sizes": [5],
|
||||||
|
"routes": ["intravenous"],
|
||||||
|
"comp_ranges": {
|
||||||
|
"weight_ratio_min": 10,
|
||||||
|
"weight_ratio_max": 15,
|
||||||
|
"cationic_mol_min": 40,
|
||||||
|
"cationic_mol_max": 50,
|
||||||
|
"phospholipid_mol_min": 10,
|
||||||
|
"phospholipid_mol_max": 20,
|
||||||
|
"cholesterol_mol_min": 28,
|
||||||
|
"cholesterol_mol_max": 45,
|
||||||
|
"peg_mol_min": 1,
|
||||||
|
"peg_mol_max": 5
|
||||||
|
},
|
||||||
|
"scoring_weights": {
|
||||||
|
"biodist_weight": 1,
|
||||||
|
"delivery_weight": 0,
|
||||||
|
"size_weight": 0,
|
||||||
|
"ee_class_weights": [0, 0, 0],
|
||||||
|
"pdi_class_weights": [0, 0],
|
||||||
|
"toxic_class_weights": [0, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
187
server_test_cases/run_cases.sh
Executable file
187
server_test_cases/run_cases.sh
Executable file
@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CASE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
API_PORT=${API_PORT:-18000}
|
||||||
|
API_URL=${API_URL:-http://127.0.0.1:${API_PORT}}
|
||||||
|
GPU_INDEX=${GPU_INDEX:-0}
|
||||||
|
CURL_TIMEOUT=${CURL_TIMEOUT:-1800}
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
run_case() {
|
||||||
|
local name=$1
|
||||||
|
shift
|
||||||
|
echo
|
||||||
|
echo "===== ${name} ====="
|
||||||
|
if "$@"; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo "PASS: ${name}"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo "FAIL: ${name}" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_health() {
|
||||||
|
local response=/tmp/lnp-test-health.json
|
||||||
|
curl -fsS --max-time 30 "${API_URL}/" >"${response}"
|
||||||
|
python3 - "${response}" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
assert data["status"] == "healthy", data
|
||||||
|
assert data["model_loaded"] is True, data
|
||||||
|
assert data["use_llm"] is True, data
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_organs() {
|
||||||
|
local response=/tmp/lnp-test-organs.json
|
||||||
|
curl -fsS --max-time 30 "${API_URL}/organs" >"${response}"
|
||||||
|
python3 - "${response}" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
expected = {"lymph_nodes", "heart", "liver", "spleen", "lung", "kidney", "muscle"}
|
||||||
|
assert set(data) == expected, data
|
||||||
|
print(json.dumps(data, ensure_ascii=False))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_single() {
|
||||||
|
local response=/tmp/lnp-test-single.json
|
||||||
|
curl -fsS --max-time "${CURL_TIMEOUT}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary "@${CASE_DIR}/single_valid.json" \
|
||||||
|
"${API_URL}/predict" >"${response}"
|
||||||
|
python3 - "${response}" <<'PY'
|
||||||
|
import json, math, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
required = ["biodist", "quantified_delivery", "pdi_class", "ee_class", "toxic_class"]
|
||||||
|
assert all(k in data for k in required), data
|
||||||
|
values = list(data["biodist"].values())
|
||||||
|
assert len(values) == 7, values
|
||||||
|
assert all(math.isfinite(float(x)) for x in values), values
|
||||||
|
assert abs(sum(values) - 1.0) < 1e-4, sum(values)
|
||||||
|
assert math.isfinite(float(data["quantified_delivery"]))
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
make_batch_payload() {
|
||||||
|
local count=$1
|
||||||
|
local use_llm=$2
|
||||||
|
local batch_size=$3
|
||||||
|
python3 - "${CASE_DIR}/single_valid.json" "${count}" "${use_llm}" "${batch_size}" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
item = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
print(json.dumps({
|
||||||
|
"items": [item for _ in range(int(sys.argv[2]))],
|
||||||
|
"batch_size": int(sys.argv[4]),
|
||||||
|
"use_llm": sys.argv[3].lower() == "true",
|
||||||
|
}))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_batch_response() {
|
||||||
|
local response=$1
|
||||||
|
local expected=$2
|
||||||
|
python3 - "${response}" "${expected}" <<'PY'
|
||||||
|
import json, math, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
expected = int(sys.argv[2])
|
||||||
|
assert data["n_requested"] == expected, data
|
||||||
|
assert data["n_succeeded"] == expected, data
|
||||||
|
assert data["errors"] == [], data["errors"]
|
||||||
|
assert len(data["predictions"]) == expected
|
||||||
|
for pred in data["predictions"]:
|
||||||
|
values = list(pred["biodist"].values())
|
||||||
|
assert len(values) == 7
|
||||||
|
assert all(math.isfinite(float(x)) for x in values)
|
||||||
|
assert abs(sum(values) - 1.0) < 1e-4
|
||||||
|
print(f"n_succeeded={data['n_succeeded']}, errors={len(data['errors'])}")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_batch() {
|
||||||
|
local count=$1
|
||||||
|
local use_llm=$2
|
||||||
|
local batch_size=$3
|
||||||
|
local response="/tmp/lnp-test-batch-${count}-${use_llm}.json"
|
||||||
|
local payload before peak current curl_pid start_ms end_ms
|
||||||
|
|
||||||
|
payload=$(make_batch_payload "${count}" "${use_llm}" "${batch_size}")
|
||||||
|
before=$(nvidia-smi --id="${GPU_INDEX}" --query-gpu=memory.used \
|
||||||
|
--format=csv,noheader,nounits | head -n 1 | tr -d ' ')
|
||||||
|
peak=${before}
|
||||||
|
start_ms=$(date +%s%3N)
|
||||||
|
|
||||||
|
curl -fsS --max-time "${CURL_TIMEOUT}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "${payload}" "${API_URL}/predict/batch" >"${response}" &
|
||||||
|
curl_pid=$!
|
||||||
|
while kill -0 "${curl_pid}" 2>/dev/null; do
|
||||||
|
current=$(nvidia-smi --id="${GPU_INDEX}" --query-gpu=memory.used \
|
||||||
|
--format=csv,noheader,nounits | head -n 1 | tr -d ' ')
|
||||||
|
if [ "${current}" -gt "${peak}" ]; then peak=${current}; fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
wait "${curl_pid}"
|
||||||
|
end_ms=$(date +%s%3N)
|
||||||
|
|
||||||
|
validate_batch_response "${response}" "${count}"
|
||||||
|
echo "use_llm=${use_llm}, count=${count}, outer_batch=${batch_size}"
|
||||||
|
echo "GPU used before=${before} MiB, observed peak=${peak} MiB, delta=$((peak - before)) MiB"
|
||||||
|
echo "elapsed=$((end_ms - start_ms)) ms"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_mixed_errors() {
|
||||||
|
local response=/tmp/lnp-test-mixed.json
|
||||||
|
curl -fsS --max-time "${CURL_TIMEOUT}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary "@${CASE_DIR}/mixed_valid_invalid.json" \
|
||||||
|
"${API_URL}/predict/batch" >"${response}"
|
||||||
|
python3 - "${response}" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
assert data["n_requested"] == 5, data
|
||||||
|
assert data["n_succeeded"] == 2, data
|
||||||
|
assert len(data["errors"]) == 3, data
|
||||||
|
assert {x["index"] for x in data["errors"]} == {2, 3, 4}, data["errors"]
|
||||||
|
print(json.dumps(data["errors"], ensure_ascii=False, indent=2))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_optimize() {
|
||||||
|
local response=/tmp/lnp-test-optimize.json
|
||||||
|
curl -fsS --max-time "${CURL_TIMEOUT}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary "@${CASE_DIR}/optimize_smoke.json" \
|
||||||
|
"${API_URL}/optimize" >"${response}"
|
||||||
|
python3 - "${response}" <<'PY'
|
||||||
|
import json, math, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
assert data["target_organ"] == "liver", data
|
||||||
|
assert 1 <= len(data["formulations"]) <= 3, data
|
||||||
|
for row in data["formulations"]:
|
||||||
|
assert math.isfinite(float(row["target_biodist"])), row
|
||||||
|
assert abs(sum(float(x) for x in row["all_biodist"].values()) - 1.0) < 1e-4, row
|
||||||
|
print(f"optimize returned {len(data['formulations'])} formulations")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_case "health" validate_health
|
||||||
|
run_case "available organs" validate_organs
|
||||||
|
run_case "single prediction with LLM" validate_single
|
||||||
|
run_case "64 items without LLM (throughput baseline)" run_batch 64 false 64
|
||||||
|
run_case "32 items with LLM (VRAM regression)" run_batch 32 true 32
|
||||||
|
run_case "mixed valid/invalid items" validate_mixed_errors
|
||||||
|
run_case "small optimize search" validate_optimize
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===== SUMMARY ====="
|
||||||
|
echo "passed=${PASS}, failed=${FAIL}"
|
||||||
|
if [ "${FAIL}" -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
146
server_test_cases/run_lnp_batch_stress.sh
Executable file
146
server_test_cases/run_lnp_batch_stress.sh
Executable file
@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
CASE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
API_PORT=${API_PORT:-18000}
|
||||||
|
API_URL=${API_URL:-http://127.0.0.1:${API_PORT}}
|
||||||
|
GPU_INDEX=${GPU_INDEX:-0}
|
||||||
|
CURL_TIMEOUT=${CURL_TIMEOUT:-3600}
|
||||||
|
MAX_GPU_MIB=${MAX_GPU_MIB:-0}
|
||||||
|
PROFILE=${1:-repro}
|
||||||
|
RESULT_CSV=${RESULT_CSV:-"${CASE_DIR}/vram-results-$(date +%Y%m%d-%H%M%S).csv"}
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null || ! command -v python3 >/dev/null; then
|
||||||
|
echo "curl and python3 are required" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! command -v nvidia-smi >/dev/null; then
|
||||||
|
echo "nvidia-smi is required for the GPU memory test" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if ! curl -fsS --max-time 30 "${API_URL}/" >/tmp/lnp-stress-health.json; then
|
||||||
|
echo "API is not reachable: ${API_URL}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
gpu_used_mib() {
|
||||||
|
nvidia-smi --id="${GPU_INDEX}" --query-gpu=memory.used --format=csv,noheader,nounits \
|
||||||
|
| head -n 1 | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
now_ms() {
|
||||||
|
python3 -c 'import time; print(int(time.time() * 1000))'
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_response() {
|
||||||
|
local response=$1 expected=$2
|
||||||
|
python3 - "${response}" "${expected}" <<'PY'
|
||||||
|
import json, math, sys
|
||||||
|
data = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||||
|
expected = int(sys.argv[2])
|
||||||
|
assert data["n_requested"] == expected, data
|
||||||
|
assert data["n_succeeded"] == expected, data
|
||||||
|
assert data["errors"] == [], data["errors"]
|
||||||
|
assert len(data["predictions"]) == expected, len(data["predictions"])
|
||||||
|
for pred in data["predictions"]:
|
||||||
|
values = list(pred["biodist"].values())
|
||||||
|
assert len(values) == 7
|
||||||
|
assert all(math.isfinite(float(value)) for value in values)
|
||||||
|
assert abs(sum(values) - 1.0) < 1e-4, sum(values)
|
||||||
|
print(f"validated {expected} predictions")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
run_payload() {
|
||||||
|
local label=$1 payload=$2 expected=$3
|
||||||
|
local response="/tmp/lnp-${label}-response.json"
|
||||||
|
local http_file="/tmp/lnp-${label}-http.txt"
|
||||||
|
local before peak current curl_pid curl_status http_code start_ms end_ms elapsed delta status
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===== ${label} ====="
|
||||||
|
before=$(gpu_used_mib)
|
||||||
|
peak=${before}
|
||||||
|
start_ms=$(now_ms)
|
||||||
|
|
||||||
|
curl -sS --max-time "${CURL_TIMEOUT}" -o "${response}" -w '%{http_code}' \
|
||||||
|
-H 'Content-Type: application/json' --data-binary "@${payload}" \
|
||||||
|
"${API_URL}/predict/batch" >"${http_file}" &
|
||||||
|
curl_pid=$!
|
||||||
|
while kill -0 "${curl_pid}" 2>/dev/null; do
|
||||||
|
current=$(gpu_used_mib 2>/dev/null || echo 0)
|
||||||
|
if [[ "${current}" =~ ^[0-9]+$ ]] && [ "${current}" -gt "${peak}" ]; then
|
||||||
|
peak=${current}
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
wait "${curl_pid}"
|
||||||
|
curl_status=$?
|
||||||
|
end_ms=$(now_ms)
|
||||||
|
http_code=$(cat "${http_file}" 2>/dev/null || echo 000)
|
||||||
|
elapsed=$((end_ms - start_ms))
|
||||||
|
delta=$((peak - before))
|
||||||
|
status=PASS
|
||||||
|
|
||||||
|
if [ "${curl_status}" -ne 0 ] || [ "${http_code}" != "200" ]; then
|
||||||
|
status=FAIL
|
||||||
|
echo "request failed: curl_status=${curl_status}, HTTP=${http_code}" >&2
|
||||||
|
sed -n '1,30p' "${response}" 2>/dev/null || true
|
||||||
|
elif ! validate_response "${response}" "${expected}"; then
|
||||||
|
status=FAIL
|
||||||
|
elif [ "${MAX_GPU_MIB}" -gt 0 ] && [ "${peak}" -gt "${MAX_GPU_MIB}" ]; then
|
||||||
|
status=FAIL
|
||||||
|
echo "peak ${peak} MiB exceeds MAX_GPU_MIB=${MAX_GPU_MIB}" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${label}: status=${status}, before=${before} MiB, peak=${peak} MiB, delta=${delta} MiB, elapsed=${elapsed} ms"
|
||||||
|
printf '%s,%s,%s,%s,%s,%s,%s\n' \
|
||||||
|
"${label}" "${status}" "${before}" "${peak}" "${delta}" "${elapsed}" "${http_code}" >>"${RESULT_CSV}"
|
||||||
|
[ "${status}" = PASS ]
|
||||||
|
}
|
||||||
|
|
||||||
|
make_payload() {
|
||||||
|
local count=$1 outer_batch=$2 use_llm=$3 output=$4
|
||||||
|
local llm_flag=--use-llm
|
||||||
|
[ "${use_llm}" = false ] && llm_flag=--no-use-llm
|
||||||
|
python3 "${CASE_DIR}/generate_mock_batch.py" \
|
||||||
|
--count "${count}" --batch-size "${outer_batch}" "${llm_flag}" \
|
||||||
|
--source-csv "${CASE_DIR}/../data/interim/internal.csv" --output "${output}"
|
||||||
|
}
|
||||||
|
|
||||||
|
printf 'case,status,before_mib,peak_mib,delta_mib,elapsed_ms,http_code\n' >"${RESULT_CSV}"
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
case "${PROFILE}" in
|
||||||
|
repro)
|
||||||
|
run_payload "llm-32-outer32" "${CASE_DIR}/mock_batch_32_llm.json" 32 || FAILED=1
|
||||||
|
run_payload "llm-500-outer32" "${CASE_DIR}/mock_batch_500_llm.json" 500 || FAILED=1
|
||||||
|
;;
|
||||||
|
ladder)
|
||||||
|
for count in 1 8 16 32 64 128; do
|
||||||
|
payload="/tmp/lnp-ladder-${count}.json"
|
||||||
|
make_payload "${count}" "${count}" true "${payload}"
|
||||||
|
run_payload "llm-${count}-outer${count}" "${payload}" "${count}" || FAILED=1
|
||||||
|
curl -fsS --max-time 30 "${API_URL}/" >/dev/null || break
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
full)
|
||||||
|
run_payload "baseline-no-llm-128" "${CASE_DIR}/mock_batch_128_no_llm.json" 128 || FAILED=1
|
||||||
|
for count in 1 8 16 32 64 128; do
|
||||||
|
payload="/tmp/lnp-ladder-${count}.json"
|
||||||
|
make_payload "${count}" "${count}" true "${payload}"
|
||||||
|
run_payload "llm-${count}-outer${count}" "${payload}" "${count}" || FAILED=1
|
||||||
|
curl -fsS --max-time 30 "${API_URL}/" >/dev/null || break
|
||||||
|
done
|
||||||
|
curl -fsS --max-time 30 "${API_URL}/" >/dev/null \
|
||||||
|
&& run_payload "llm-500-outer32" "${CASE_DIR}/mock_batch_500_llm.json" 500 || FAILED=1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [repro|ladder|full]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Results: ${RESULT_CSV}"
|
||||||
|
exit "${FAILED}"
|
||||||
10
server_test_cases/single_valid.json
Normal file
10
server_test_cases/single_valid.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"smiles": "CC(C)NCCNC(C)C",
|
||||||
|
"cationic_lipid_to_mrna_ratio": 10.0,
|
||||||
|
"cationic_lipid_mol_ratio": 35.0,
|
||||||
|
"phospholipid_mol_ratio": 16.0,
|
||||||
|
"cholesterol_mol_ratio": 46.5,
|
||||||
|
"peg_lipid_mol_ratio": 2.5,
|
||||||
|
"helper_lipid": "DOPE",
|
||||||
|
"route": "intravenous"
|
||||||
|
}
|
||||||
33
server_test_cases/ui_batch_screening_32.csv
Normal file
33
server_test_cases/ui_batch_screening_32.csv
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name,SMILES
|
||||||
|
LNP-001,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O)CCNC(C[C@@H](CN)CC(C)C)=O
|
||||||
|
LNP-002,O=C(OCCCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCCCC)=O)CCNC(CCC(NCC1=CC=C(B(O)O)C=C1)=O)=O
|
||||||
|
LNP-003,O=C(OCCCCCCN([C@H](C(NCCCN(CCCCCCOC(CCCCCCC)=O)CCCCCCOC(CCCCCCC)=O)=O)CO)CCCCCCOC(CCCCCCC)=O)CCCCCCC
|
||||||
|
LNP-004,O=C(OCCCCCCN([C@H](C(NCCCCCCN(CCCCCCOC(CCCCC)=O)CCCCCCOC(CCCCC)=O)=O)CO)CCCCCCOC(CCCCC)=O)CCCCC
|
||||||
|
LNP-005,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN(CC1=NC=CS1)CC2=NC=CS2)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O
|
||||||
|
LNP-006,O=C(OCCCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCCCC)=O)CCCCCCN(CCCCCCN(CCC(OCCCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCCCC)=O)CCCN1CCOCC1
|
||||||
|
LNP-007,CCCCCCCCCCCC(OCCCCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCOC([C@@H](N(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CO)=O)=O
|
||||||
|
LNP-008,O=C(OCCCCCCN([C@H](C(NCCN(C)CCN(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)=O)CO)CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCCCC=C
|
||||||
|
LNP-009,CC(CNC([C@@H](N(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)CO)=O)(CN(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)C
|
||||||
|
LNP-010,O=C(NCCN(CCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CCCN
|
||||||
|
LNP-011,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN1CC(C(OC)=O)CC1=O)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O
|
||||||
|
LNP-012,CCCCCCCCCCCCOC(CCN([C@H](C(NCCCCCCN(CCC(OCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCC)=O)=O)CO)CCC(OCCCCCCCCCCCC)=O)=O
|
||||||
|
LNP-013,O=C([C@H](CO)N(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)N(CC1)CCN1C2=NC=CC=N2
|
||||||
|
LNP-014,O=C(CN(CC(O)CCCCCCCC)CC(O)CCCCCCCC)NCCCN(CC(O)CCCCCCCC)CC(O)CCCCCCCC
|
||||||
|
LNP-015,CCCCCCCCCCCC(OCCCCCCN(CCN(C)CCNC([C@@H](N(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)CO)=O)CCCCCCOC(CCCCCCCCCCC)=O)=O
|
||||||
|
LNP-016,OC[C@@H](C(NCCCCCCN(CC(O)CCCCCCCCCC)CC(O)CCCCCCCCCC)=O)N(CC(CCCCCCCCCC)O)CC(CCCCCCCCCC)O
|
||||||
|
LNP-017,O=C(OCCCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCCCC)=O)CCNC(C[C@@H](CN)CC(C)C)=O
|
||||||
|
LNP-018,O=C(OCCCCCCCC/C=C\C/C=C\CCCCC)CCN(CCC(OCCCCCCCC/C=C\C/C=C\CCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC/C=C\C/C=C\CCCCC)=O)CCC(OCCCCCCCC/C=C\C/C=C\CCCCC)=O)CCNC(CCC(NCCC1=CC=C(O)C(O)=C1)=O)=O
|
||||||
|
LNP-019,O=C(NCCCCCCN(CCCCCCCCCCCC)CCCCCCCCCCCC)[C@H](C)N(CCCCCCCCCCCC)CCCCCCCCCCCC
|
||||||
|
LNP-020,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN(CCC(F)(F)F)CCC(F)(F)F)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O
|
||||||
|
LNP-021,CCCCCCCCC(CN(CCCNC([C@@H](N(CC(CCCCCCCC)O)CC(CCCCCCCC)O)CO)=O)CC(CCCCCCCC)O)O
|
||||||
|
LNP-022,O=C([C@H](CO)N(CCCCCCOC(CCCCCCCCC=C)=O)CCCCCCOC(CCCCCCCCC=C)=O)N(CC1)CCN1C2=NC=CC=N2
|
||||||
|
LNP-023,CCCCCCCCCCCC(OCCCCCCN([C@H](C(NCCCCCCN(CCCCCCOC(CCCCCCCCCCC)=O)CCCCCCOC(CCCCCCCCCCC)=O)=O)CO)CCCCCCOC(CCCCCCCCCCC)=O)=O
|
||||||
|
LNP-024,O=C(OCCCCCCCCCCCC)CCN(CCC(OCCCCCCCCCCCC)=O)CCCCCCN(CCCCCCN(CCC(OCCCCCCCCCCCC)=O)CCC(OCCCCCCCCCCCC)=O)CCN1CCOCC1
|
||||||
|
LNP-025,O=C(CN(CCCCCCCCCCCCCC)CCCCCCCCCCCCCC)NCCCN(CCCCCCCCCCCCCC)CCCCCCCCCCCCCC
|
||||||
|
LNP-026,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCCCN(CCCCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O)CCNC(CCC(NCCC1=CC=C(O)C(O)=C1)=O)=O
|
||||||
|
LNP-027,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O)CCNC([C@@H](N)CCCCN)=O
|
||||||
|
LNP-028,O=C(CN(CCCCCCCCCCCC)CCCCCCCCCCCC)NCCCN(CCCCCCCCCCCC)CCCCCCCCCCCC
|
||||||
|
LNP-029,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCN(CC1=COC=N1)CC2=COC=N2)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O
|
||||||
|
LNP-030,O=C(OCCCCCCCC)CCN(CCC(OCCCCCCCC)=O)CCCCN(CCNC(CCCN)=O)CCCCN(CCC(OCCCCCCCC)=O)CCC(OCCCCCCCC)=O
|
||||||
|
LNP-031,O=C(CN(CCCCCCCCCC)CCCCCCCCCC)NCCCN(CCCCCCCCCC)CCCCCCCCCC
|
||||||
|
LNP-032,O=C(OCCCCCCCC/C=C\C/C=C\CCCCC)CCN(CCC(OCCCCCCCC/C=C\C/C=C\CCCCC)=O)CCCCN(CCCCN(CCC(OCCCCCCCC/C=C\C/C=C\CCCCC)=O)CCC(OCCCCCCCC/C=C\C/C=C\CCCCC)=O)CCNC(CCCN)=O
|
||||||
|
3
server_test_cases/ui_batch_screening_matrix.csv
Normal file
3
server_test_cases/ui_batch_screening_matrix.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
head,Tail-A,Tail-B
|
||||||
|
H-01,CC(C)NCCNC(C)C,CCCCCCCCCCN(C)C
|
||||||
|
H-02,CCN(CC)CCOC(=O)CCCC,CCCCCCCCCCCCN(C)C
|
||||||
|
7
server_test_cases/ui_batch_screening_mixed.csv
Normal file
7
server_test_cases/ui_batch_screening_mixed.csv
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
name,SMILES
|
||||||
|
valid-1,CC(C)NCCNC(C)C
|
||||||
|
valid-2,CCCCCCCCCCN(C)C
|
||||||
|
invalid-backend-1,C1INVALIDCCCC
|
||||||
|
filtered-short,abc
|
||||||
|
duplicate-valid-1,CC(C)NCCNC(C)C
|
||||||
|
invalid-backend-2,NOT_A_VALID_CCCC
|
||||||
|
58
tests/test_llm_memory.py
Normal file
58
tests/test_llm_memory.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""LLM 显存保护逻辑的轻量测试(不需要下载模型)。"""
|
||||||
|
|
||||||
|
from types import MethodType, SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from lnp_ml.modeling.layers.llm_prompt import LLMPromptEncoder, _positive_int_env
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_encoder(micro_batch: int = 2) -> LLMPromptEncoder:
|
||||||
|
encoder = LLMPromptEncoder.__new__(LLMPromptEncoder)
|
||||||
|
torch.nn.Module.__init__(encoder)
|
||||||
|
encoder.inference_batch_size = micro_batch
|
||||||
|
return encoder
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_inference_batch_env_falls_back() -> None:
|
||||||
|
with patch.dict("os.environ", {"LLM_INFERENCE_BATCH_SIZE": "invalid"}):
|
||||||
|
assert _positive_int_env("LLM_INFERENCE_BATCH_SIZE", 4) == 4
|
||||||
|
with patch.dict("os.environ", {"LLM_INFERENCE_BATCH_SIZE": "0"}):
|
||||||
|
assert _positive_int_env("LLM_INFERENCE_BATCH_SIZE", 4) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_softrag_inference_is_microbatched() -> None:
|
||||||
|
encoder = _empty_encoder(micro_batch=2).eval()
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_chunk(self, smiles, chem, tab, device):
|
||||||
|
calls.append((list(smiles), chem.clone(), tab.clone()))
|
||||||
|
return chem[:, 0, :] + tab[:, 0, :]
|
||||||
|
|
||||||
|
encoder._encode_softrag_chunk = MethodType(fake_chunk, encoder)
|
||||||
|
chem = torch.arange(5 * 2 * 3, dtype=torch.float32).reshape(5, 2, 3)
|
||||||
|
tab = torch.ones(5, 1, 3)
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
output = encoder._encode_softrag(
|
||||||
|
[f"SMILES-{i}" for i in range(5)], chem, tab, torch.device("cpu")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [len(smiles) for smiles, _, _ in calls] == [2, 2, 1]
|
||||||
|
assert torch.equal(output, chem[:, 0, :] + tab[:, 0, :])
|
||||||
|
|
||||||
|
|
||||||
|
def test_qwen_forward_disables_kv_cache() -> None:
|
||||||
|
encoder = _empty_encoder()
|
||||||
|
encoder._is_qwen = True
|
||||||
|
received = {}
|
||||||
|
|
||||||
|
class FakeBackbone(torch.nn.Module):
|
||||||
|
def forward(self, **kwargs):
|
||||||
|
received.update(kwargs)
|
||||||
|
return SimpleNamespace(last_hidden_state=torch.zeros(1, 1, 1))
|
||||||
|
|
||||||
|
encoder.encoder = FakeBackbone()
|
||||||
|
encoder._encoder_forward(input_ids=torch.ones(1, 1, dtype=torch.long))
|
||||||
|
assert received["use_cache"] is False
|
||||||
Loading…
x
Reference in New Issue
Block a user