Compare commits

..

2 commits

Author SHA1 Message Date
Lihatoo
5daa60a464 为历史运行任务添加终止按钮 2026-07-26 17:46:50 +08:00
Lihatoo
eb7033c496 说明设计停止条件和容器资源配置 2026-07-26 17:14:19 +08:00
4 changed files with 106 additions and 26 deletions

View file

@ -16,6 +16,77 @@ The service listens on port `18765`. The current local deployment is reverse pro
Traefik only performs TLS termination, compression, and reverse proxying for `npt`; authentication is enforced by this application.
## Deploying the independent np instance
`np.lihato.icu` runs independently on `100.64.0.11`. Do not copy the `npt`
OIDC values unchanged. In the `np-replica` service of the
`docker-compose.yml` deployed on `.11`, use:
```yaml
ports:
- "100.64.0.11:18765:18765"
environment:
NP_AUTH_REQUIRED: 1
NP_OIDC_ISSUER: https://auth.lihato.icu/application/o/nupack-account/
NP_OIDC_CLIENT_ID: np-replica-web
NP_OIDC_REDIRECT_URI: https://np.lihato.icu/auth/callback
NP_OIDC_POST_LOGOUT_URI: https://np.lihato.icu/
```
The `NP_OIDC_*` variables belong on the web service (`np-replica`); the
worker does not perform browser login. Preserve the existing calculation
settings, including `NP_WORKER_CONCURRENCY=16`,
`NP_PER_JOB_THREAD_LIMIT=4`, and `NP_NUPACK_CACHE_GB=8.0`.
After editing the Compose file on `.11`, recreate the web container:
```bash
docker compose up -d --force-recreate np-replica
```
If the deployed image predates the account/OIDC changes, rebuild both
application containers instead:
```bash
docker compose up -d --build --force-recreate np-replica np-worker
```
Verify the result:
```bash
curl -I https://np.lihato.icu/
curl -I 'https://np.lihato.icu/auth/login?next=%2F'
```
The first response should redirect to `/auth/login?next=%2F`. The second
`Location` header must contain all of the following:
- `client_id=np-replica-web`
- `redirect_uri=https%3A%2F%2Fnp.lihato.icu%2Fauth%2Fcallback`
- `code_challenge_method=S256`
If it contains `npt-replica-web` or `npt.lihato.icu`, the `.11` container
is still running with the wrong environment and must be recreated.
## 容器资源配置
当前 `docker-compose.yml` 面向 64 核 / 64 GB 级别的服务器配置,服务拆成三个容器:
- `redis`:只保存实时队列、运行状态和会话缓存,限制为 `mem_limit: 3g`。Redis 开启 AOF数据写入 `./runtime/redis`
- `np-replica`Web/API 容器,限制为 `mem_limit: 4g`,负责页面、登录、历史记录、任务提交和状态查询。它不应该承担大规模计算。
- `np-worker`:后台计算容器,限制为 `mem_limit: 56g`,负责实际 NUPACK 计算。大任务应该由这个容器消耗 CPU 和内存。
计算相关环境变量需要在 `np-replica``np-worker` 中保持一致:
- `NP_WORKER_CONCURRENCY=16`:最多同时执行 16 个后台任务,超过后进入 Redis 队列等待。
- `NP_PER_JOB_THREAD_LIMIT=4`:单个任务最多使用 4 个 native 计算线程,同时写入 `OMP_NUM_THREADS``OPENBLAS_NUM_THREADS``MKL_NUM_THREADS``NUMEXPR_NUM_THREADS``VECLIB_MAXIMUM_THREADS``GOTO_NUM_THREADS`,并设置 `nupack.config.threads`
- `NP_NUPACK_CACHE_GB=8.0`:单个 NUPACK 进程可使用的缓存上限。
- `NP_JOB_TTL_SECONDS=3600`Redis 中实时任务状态的保留时间;长期历史记录写入 SQLite。
理论上当前峰值为 `16 * 4 = 64` 个 native 计算线程。若机器被压满,优先把 `NP_WORKER_CONCURRENCY``16` 降到 `8``4`;如果单任务仍过重,再把 `NP_PER_JOB_THREAD_LIMIT``4` 降到 `2``1``np-replica` 的内存不建议调高来跑计算,应该把计算压力留给 `np-worker`
设计任务默认使用官方行为:固定序列 target 也参与 `tube_design` / `complex_design`。如果确认某些 target 完全固定、只需要展示结果、不需要参与优化,可在页面中把 “固定目标策略” 改为 “剥离固定目标以提速”,这样会减少 off-target 集合和优化搜索量。
## Endpoints
- `GET /`
@ -52,18 +123,3 @@ Live jobs and sessions are stored in Redis when `NP_REDIS_URL` is enabled. Accou
- For the independent `np` deployment on `100.64.0.11`, set `NP_OIDC_ISSUER=https://auth.lihato.icu/application/o/nupack-account/`, `NP_OIDC_CLIENT_ID=np-replica-web`, `NP_OIDC_REDIRECT_URI=https://np.lihato.icu/auth/callback`, and `NP_OIDC_POST_LOGOUT_URI=https://np.lihato.icu/`.
- Redis stores the live queue and login sessions. SQLite WAL at `/data/np-replica.sqlite3` stores users, owned jobs, compressed inputs/results/errors, usage totals, and durable share links.
- History and job APIs enforce ownership by the Authentik OIDC subject. Public share links expose only the selected record and can be disabled or given an expiry by its owner.
现在 `docker-compose.yml` 面向 64 核 / 64G WSL 服务器的并发策略是:
- 最多同时跑 16 个任务
- 每个任务最多用 4 个计算线程
- 后续任务进入队列等待
- 每个任务的 NUPACK 缓存上限为 8 GB
- 理论上最多占用约 64 个 native 计算线程
如果后面你发现机器还会被压满,最直接的调法就是在 docker-compose.yml 里继续压:
- 把 NP_WORKER_CONCURRENCY 改小
- 或保持并发不变,把 NP_PER_JOB_THREAD_LIMIT 改成 2 或 1

View file

@ -3,7 +3,7 @@ services:
image: redis:7.4.8-bookworm
container_name: np-redis
restart: always
mem_limit: 2g
mem_limit: 3g
command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec", "--save", "60", "1"]
volumes:
- ./runtime/redis:/data

View file

@ -1687,6 +1687,7 @@ A+B</textarea>
<label>
<span data-i18n="design_stop_condition"></span>
<input id="designStopCondition" type="number" value="0.02" min="0.001" max="0.99" step="0.01" />
<small class="minor-note" data-i18n="design_stop_condition_hint"></small>
</label>
<label>
<span data-i18n="design_max_time_hours"></span>
@ -1706,8 +1707,8 @@ A+B</textarea>
<label>
<span data-i18n="design_fixed_target_policy"></span>
<select id="designFixedTargetPolicy">
<option value="exclude_from_optimization" data-i18n="design_fixed_target_exclude"></option>
<option value="include" data-i18n="design_fixed_target_include"></option>
<option value="exclude_from_optimization" data-i18n="design_fixed_target_exclude"></option>
</select>
</label>
</div>
@ -2223,14 +2224,15 @@ A+B</textarea>
design_help: "Design 按 NUPACK 的 `Domain -> TargetStrand -> TargetComplex/TargetTube` 关系组织。可先定义 domain 约束,再在链输入中用空格分隔的 domain composition 组装链;也可让某条链继续直接使用整链 IUPAC 约束。",
design_guide_btn: "打开 Design 教学",
design_stop_condition: "停止条件 f_stop",
design_stop_condition_hint: "f_stop 越小越严格,可能极慢;若长时间无法达到该缺陷阈值,设计会持续搜索。",
design_max_time_hours: "最大设计时间 (小时)",
design_seed_label: "随机种子",
design_wobble_label: "Wobble mutations",
design_wobble_allow: "允许",
design_wobble_prohibit: "禁止",
design_fixed_target_policy: "固定目标策略",
design_fixed_target_exclude: "剥离出优化",
design_fixed_target_include: "参与优化",
design_fixed_target_exclude: "剥离固定目标以提速",
design_fixed_target_include: "参与优化(官方默认)",
design_tab_targets: "Target Tubes",
design_tab_hard: "Hard Constraints",
design_tab_soft: "Soft Constraints",
@ -2357,6 +2359,9 @@ A+B</textarea>
history_empty: "还没有历史任务。",
history_load: "载入",
history_delete: "删除",
history_cancel: "终止",
history_cancel_confirm: "确定要终止这个正在运行的任务吗?终止后不可恢复。",
history_cancel_success: "任务终止请求已发送",
history_mode_tube: "Tube",
history_mode_complex: "Complex",
count_complexes: "个复合物",
@ -2574,14 +2579,15 @@ A+B</textarea>
design_help: "Design follows the NUPACK `Domain -> TargetStrand -> TargetComplex/TargetTube` hierarchy. You can define domains first and then compose strands from domain tokens, while still allowing any strand to stay as a whole-strand IUPAC constraint when needed.",
design_guide_btn: "Open Design Guide",
design_stop_condition: "Stop Condition f_stop",
design_stop_condition_hint: "Smaller f_stop values are stricter and can be extremely slow; design keeps searching until this defect threshold is reached.",
design_max_time_hours: "Max Design Time (hours)",
design_seed_label: "Random Seed",
design_wobble_label: "Wobble Mutations",
design_wobble_allow: "Allow",
design_wobble_prohibit: "Prohibit",
design_fixed_target_policy: "Fixed Target Policy",
design_fixed_target_exclude: "Exclude from Optimization",
design_fixed_target_include: "Include in Optimization",
design_fixed_target_exclude: "Exclude Fixed Targets for Speed",
design_fixed_target_include: "Include in Optimization (Official Default)",
design_tab_targets: "Target Tubes",
design_tab_hard: "Hard Constraints",
design_tab_soft: "Soft Constraints",
@ -2708,6 +2714,9 @@ A+B</textarea>
history_empty: "No saved jobs yet.",
history_load: "Load",
history_delete: "Delete",
history_cancel: "Terminate",
history_cancel_confirm: "Terminate this running job? This cannot be undone.",
history_cancel_success: "Job termination requested",
history_mode_tube: "Tube",
history_mode_complex: "Complex",
count_complexes: "complexes",
@ -3964,6 +3973,17 @@ A+B</textarea>
await refreshAccountWorkspace();
}
async function cancelHistoryJob(id) {
if (!window.confirm(t("history_cancel_confirm"))) return;
const { response, data } = await fetchJsonOrThrow(`/api/jobs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
if (!response.ok || data.status !== "success") {
throw new Error(data?.error || `Cancel failed with HTTP ${response.status}`);
}
setStatus(formatStatus(t("history_cancel_success")));
await refreshAccountWorkspace();
refreshHealth();
}
async function fetchAllHistory() {
const items = [];
let offset = 0;
@ -4070,7 +4090,7 @@ A+B</textarea>
document.getElementById("designSeed").value = payload.design?.seed ?? 0;
document.getElementById("designWobble").value = String(payload.design?.wobble_mutations ?? false);
document.getElementById("designMaxTimeHours").value = ((payload.design?.max_time_seconds ?? 0) / 3600);
document.getElementById("designFixedTargetPolicy").value = payload.design?.fixed_target_policy ?? "exclude_from_optimization";
document.getElementById("designFixedTargetPolicy").value = payload.design?.fixed_target_policy ?? "include";
const payloadCompute = Array.isArray(payload.compute) ? payload.compute : [];
document.querySelectorAll(".check input").forEach((input) => {
input.checked = payloadCompute.includes(input.value);
@ -4177,6 +4197,7 @@ A+B</textarea>
<button class="secondary" data-history-load="${item.job_id}" type="button">${t("history_load")}</button>
<button class="secondary" data-history-export="${item.job_id}" type="button">${t("history_export_one")}</button>
<button class="secondary" data-history-share="${item.job_id}" type="button">${t("history_share")}</button>
${["queued", "running"].includes(item.status) ? `<button class="danger" data-history-cancel="${item.job_id}" type="button">${t("history_cancel")}</button>` : ""}
${["queued", "running", "cancel_requested"].includes(item.status) ? "" : `<button class="secondary" data-history-delete="${item.job_id}" type="button">${t("history_delete")}</button>`}
</div>
</div>
@ -4193,6 +4214,9 @@ A+B</textarea>
historyList.querySelectorAll("[data-history-delete]").forEach((button) => {
button.addEventListener("click", () => deleteHistory(button.dataset.historyDelete).catch((error) => setStatus(error.message, true)));
});
historyList.querySelectorAll("[data-history-cancel]").forEach((button) => {
button.addEventListener("click", () => cancelHistoryJob(button.dataset.historyCancel).catch((error) => setStatus(`${t("cancel_job_failed")}: ${error.message}`, true)));
});
historyList.querySelectorAll("[data-history-export]").forEach((button) => {
button.addEventListener("click", () => exportHistoryItem(button.dataset.historyExport).catch((error) => setStatus(error.message, true)));
});
@ -6176,7 +6200,7 @@ A+B</textarea>
document.getElementById("designSeed").value = example.design?.seed ?? 0;
document.getElementById("designWobble").value = String(example.design?.wobble_mutations ?? false);
document.getElementById("designMaxTimeHours").value = ((example.design?.max_time_seconds ?? 0) / 3600);
document.getElementById("designFixedTargetPolicy").value = example.design?.fixed_target_policy ?? "exclude_from_optimization";
document.getElementById("designFixedTargetPolicy").value = example.design?.fixed_target_policy ?? "include";
document.getElementById("complexesText").value = example.complexes_text;
const exampleCompute = Array.isArray(example.compute) ? example.compute : [];
document.querySelectorAll(".check input").forEach((input) => {

View file

@ -371,7 +371,7 @@ def parse_design_options(payload):
"seed": int(raw.get("seed", 0)),
"wobble_mutations": bool(raw.get("wobble_mutations", False)),
"max_time_seconds": int(raw.get("max_time_seconds", 0)),
"fixed_target_policy": str(raw.get("fixed_target_policy", "exclude_from_optimization")).strip().lower(),
"fixed_target_policy": str(raw.get("fixed_target_policy", "include")).strip().lower(),
}
if options["trials"] < 1 or options["trials"] > 8:
raise ValueError("design trials must be between 1 and 8.")
@ -2245,7 +2245,7 @@ EXAMPLE_PAYLOAD = {
"seed": 0,
"wobble_mutations": False,
"max_time_seconds": 0,
"fixed_target_policy": "exclude_from_optimization",
"fixed_target_policy": "include",
},
"design_domains": [
{"name": "a", "sequence": "N10"},
@ -2307,7 +2307,7 @@ DESIGN_TUBE_EXAMPLE_PAYLOAD = {
"seed": 1,
"wobble_mutations": False,
"max_time_seconds": 0,
"fixed_target_policy": "exclude_from_optimization",
"fixed_target_policy": "include",
},
"design_domains": [
{"name": "a", "sequence": "N10"},