بهروزرسانی وزنهای مدل در محیط ایزوله، بدون شکستن ایزولاسیون
رویهای کامل برای عبور دادن وزنهای مدل از مرز یک محیط ایزوله: ساخت بسته، تأیید مجموع کنترلی و امضا، رد قطعی فرمتهای اجراشونده، آینهکردن رجیستری، انتشار مرحلهای از راه جدول تنظیمات، بازگردانی، و ردی که از حسابرسی جان سالم به در ببرد.
چرا این نوشته
یک رویهٔ اجراشدنی و کامل برای بهروزرسانی مدل در محیط ایزوله — فهرستنامه، امضای جدا، دروازهٔ فرمت، آینهکردن بلابهای Ollama، انتشار و بازگردانی از راه جدول تنظیمات — که از اصول اولیه ساخته شده، چون تقریباً هیچ فروشندهای گامهای میان «وزنها را دانلود کنید» و «کلاستر ایزوله دارد سرویس میدهد» را مستند نکرده است.
استقرار ایزوله را معمولاً با یک تصویر توضیح میدهند: رکی که کابل شبکهاش را کشیدهاند. این تصویر گمراهکننده است. ایزولاسیون خاصیت یک ماشین نیست، خاصیت یک رویه است — و رویهها زیر فشار عملیات میشکنند. لحظهای که بهروزرسانی مدل به کاری تکراری تبدیل شود، کسی دنبال میانبر میگردد، و میانبر همیشه همان یکی است: لپتاپی که به هر دو شبکه وصل شده.
آنچه در ادامه میآید، رویهای است که نیاز به آن میانبر را از بین میبرد: چگونه مجموعهٔ تازهای از وزنهای مدل را به داخل محیط ایزوله ببریم، آن را تأیید کنیم، منتشرش کنیم، در صورت لزوم برگردانیم، و ردی از خود به جا بگذاریم که در جلسهٔ حسابرسی دوام بیاورد.
فرض این نوشته یک استقرار واقعاً بریده از شبکه است: بدون پراکسی، بدون آینهٔ مجاز، بدون «فقط همین یک میزبان». اگر پراکسی خروجی دارید، بیشتر این مطالب همچنان درست است و حدود یکسوم کار حذف میشود.
دامنهٔ صادقانه
ما استقرار ایزوله را بهعنوان یک مدل استقرار پشتیبانی میکنیم. آنچه میخوانید رویهای است که خودمان را به آن پایبند میدانیم، ساختهشده از اصول اولیه و از تجربهٔ اجرای مدلهای خودمیزبان — نه گزارش یک نصب ایزولهٔ تحویلشده، و قرار هم نیست آنطور معرفیاش کنیم.
نامی که هنوز نداریم
پیش از هر بحث فنی، یک مسئلهٔ واژگانی هست که بهتر است آشکارا حل شود تا پنهانی دور زده شود. «air gap» در فارسی معادل جاافتادهای ندارد. ترجمهٔ لفظی «فاصلهٔ هوایی» هیچ معنایی منتقل نمیکند، و در اتاقهای عملیات ایران عملاً «شبکهٔ ایزوله»، «محیط آفلاین» یا خودِ «ایرگپ» شنیده میشود.
در این نوشته دو واژه را به کار میبریم و هر دو را همینجا اعلام میکنیم. برای صفت، محیط ایزوله — چون همان چیزی است که تیمهای زیرساخت ایرانی واقعاً میگویند و ساختن واژهٔ تازه برای مفهومی که نامی دارد، فقط خواندن را سخت میکند. برای اسم — یعنی خودِ آن شکاف — معادلی پیشنهاد میکنیم که به نظرمان دقیقتر است: گسست شبکهای (air gap). «گسست» بر بریدگی عمدی دلالت دارد، نه بر خرابی، و همین تمایز کل بحث است. یک شبکهٔ قطعشده اتفاق است؛ گسست شبکهای تصمیم است.
بقیهٔ اصطلاحها را هم به همین شیوه، در نخستین کاربرد با اصل انگلیسی میآوریم و بعد فارسی را ثابت نگه میداریم.
مرز را آنچه از آن عبور میکند تعریف میکند، نه کابلی که وصل نیست
گسست شبکهای روی یک قاعده زنده میماند: هر بایتی که از مرز عبور میکند، در سمت بیرون فهرست، درهمسازی و امضا میشود، و در سمت درون پیش از آنکه چیزی اجرایش کند تأیید میشود. کابل شبکه مرز نیست. مرز مجموعهٔ رویدادهای انتقال است، و هر رویداد انتقال به یک نام، یک فهرستنامه (manifest)، یک امضا و یک سند نیاز دارد.
این بازتعریف اهمیت دارد، چون بهروزرسانی مدل آزمون «کابل وصل است یا نه» را بهسادگی رد میکند و در آزمون «چه چیزی عبور کرد» مدام مردود میشود. یک پوشهٔ مدل، یک فایل نیست. وزنها هست، توکنایزر هست، پیکربندی هست، قالب گفتوگو هست، گاهی ماژول پایتونی هست که بارگذار آن را import میکند — و اگر بیدقت باشید، یک pickle هست که همان لحظهٔ بارگذاری کد دلخواه اجرا میکند. رفتار با این مجموعه بهعنوان «فقط چند فایل وزن» همان راهی است که گسست شبکهای را به یک حادثهٔ کند در زنجیرهٔ تأمین تبدیل میکند.
چه چیزهایی واقعاً باید از مرز عبور کنند
شش دستهٔ متفاوت از قلمها در یک بهروزرسانی مدل از مرز عبور میکنند و پروفایل خطرشان یکسان نیست. تنها راه صادق نگهداشتن فهرست، بستهبندی آنها با هم و تأییدشان بهعنوان یک واحد است.
| قلم | نمونه | خطر در صورت تأییدنشدن | تناوب بهروزرسانی |
|---|---|---|---|
| فایلهای تانسور | model-00001-of-00030.safetensors | کم، اگر safetensors باشد؛ اجرای کد دلخواه اگر pickle باشد | هر نسخهٔ مدل |
| توکنایزر و پیکربندی | tokenizer.json، config.json | افت کیفیت خاموش، از توکنایزر ناهمخوان | هر نسخهٔ مدل |
| قالب گفتوگو | chat_template.jinja، دستور TEMPLATE در Modelfile | رانش قالب؛ افت پیروی از دستور | هر نسخهٔ مدل |
| کد مدلسازی | modeling_*.py سفارشی | اجرای کد دلخواه هنگام بارگذاری | نادر، پرخطر |
| زمان اجرا | باینری Ollama، ویل vLLM، کتابخانههای CUDA | تصرف کامل میزبان | حداکثر فصلی |
| سند تبارنامه | فهرستنامه، امضا، متن پروانه | نمیتوانید بگویید «این از کجا آمد» | هر انتقال |
جداکردن زمان اجرا از وزنها پرارزشترین تصمیم این جدول است. وزنها ماهانه عوض میشوند؛ زمان اجرای استنتاج باید فصلی و از مسیر تأیید کندتری عوض شود. بستهبندی این دو با هم، هر تعویض مدل را به یک بازبینی کامل زمان اجرا گره میزند، و نتیجهٔ قابلپیشبینی این است که آدمها بهکلی از بهروزرسانی مدل دست میکشند.
بسته را در سمت متصل بساز، و بگذار خودش را توصیف کند
بستهٔ انتقال (bundle) یکبار، در سمت متصل، و با اسکریپت ساخته میشود — هرگز دستی. پوشهای که دستی سرِ هم شده فهرستنامه ندارد، و بستهای بدون فهرستنامه را نمیتوان تأیید کرد؛ فقط میتوان به آن اعتماد کرد.
#!/usr/bin/env bash
# build-bundle.sh — CONNECTED side. huggingface_hub >= 0.24 (newer releases alias
# `huggingface-cli download` as `hf download`), gnupg >= 2.2, coreutils.
set -euo pipefail
REPO="${1:?usage: build-bundle.sh <hf-repo-id> <version-tag>}"
TAG="${2:?}"
OUT="bundle/${TAG}"
SIGNER="releases@example.internal"
mkdir -p "${OUT}/weights"
# 1. Pull tensors and metadata only. Explicitly refuse pickle formats.
huggingface-cli download "${REPO}" \
--local-dir "${OUT}/weights" \
--include "*.safetensors" "*.safetensors.index.json" "*.json" "*.txt" \
"*.model" "*.jinja" "LICENSE*" \
--exclude "*.bin" "*.pt" "*.pth" "*.pkl" "*.ckpt" "*.h5" "*.msgpack"
# 2. Record where this came from, at the commit level — not the tag level.
# Tags move. Commit hashes do not.
REV="$(huggingface-cli scan-cache -v | awk -v r="${REPO}" '$1==r {print $NF; exit}')"
cat > "${OUT}/PROVENANCE.json" <<JSON
{
"repo": "${REPO}",
"revision": "${REV}",
"bundle_tag": "${TAG}",
"built_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"built_by": "$(id -un)@$(hostname -f)",
"runtime_included": false
}
JSON
# 3. One manifest over everything, in a stable order.
( cd "${OUT}" && find . -type f ! -name 'MANIFEST.sha256*' -print0 \
| sort -z | xargs -0 sha256sum > MANIFEST.sha256 )
# 4. Detached signature over the manifest only. Signing the manifest signs
# every file in it, transitively, and stays a one-line verification.
gpg --detach-sign --armor --local-user "${SIGNER}" \
--output "${OUT}/MANIFEST.sha256.asc" "${OUT}/MANIFEST.sha256"
echo "bundle ready: ${OUT} ($(du -sh "${OUT}" | cut -f1))"
دو جزئیات در این اسکریپت باربر است. دانلود به یک بازبینی کامیت (commit revision) سنجاق میشود، نه به شاخه یا برچسب، چون برچسبها روی هابهای عمومی مدل تغییرپذیرند و «مردادماه از main کشیدیم» سند تبارنامه نیست. و امضا بهجای تکتک فایلها، فهرستنامه را میپوشاند؛ پس تأیید یک فرمان باقی میماند که یا کاملاً قبول میشود یا کاملاً رد.
پیش از آنکه بایتها به میزبان GPU برسند، تأیید کن
تأیید روی یک میزبان قرنطینه در داخل مرز اجرا میشود که نه GPU دارد و نه زمان اجرای استنتاج روی آن نصب است. تنها وظیفهٔ آن رد کردن بستههای خراب است، پس نباید اصلاً توان اجرای یکی از آنها را داشته باشد.
#!/usr/bin/env bash
# verify-bundle.sh — ISOLATED side, on the quarantine host.
set -euo pipefail
BUNDLE="${1:?usage: verify-bundle.sh <bundle-dir>}"
# 1. Signature. The release public key was imported once, out of band,
# and its fingerprint is recorded in the change management system.
gpg --verify "${BUNDLE}/MANIFEST.sha256.asc" "${BUNDLE}/MANIFEST.sha256"
# 2. Content. --check fails on any missing or altered file.
( cd "${BUNDLE}" && sha256sum --quiet --check MANIFEST.sha256 )
# 3. Format gate. Pickle-backed tensors execute Python on load; safetensors
# cannot. This is a hard refusal, not a warning.
python3 - "${BUNDLE}" <<'PY'
import pathlib, sys
BANNED = {".bin", ".pt", ".pth", ".pkl", ".ckpt", ".h5", ".msgpack"}
bad = [p for p in pathlib.Path(sys.argv[1]).rglob("*") if p.suffix in BANNED]
if bad:
sys.exit("REFUSED: pickle-format tensors present:\n " +
"\n ".join(str(p) for p in bad))
code = [p for p in pathlib.Path(sys.argv[1]).rglob("modeling_*.py")]
if code:
sys.exit("REFUSED: custom modelling code requires separate review:\n " +
"\n ".join(str(p) for p in code))
print("format gate: clean")
PY
echo "bundle ${BUNDLE}: VERIFIED"
رد کردن pickle بحثی نظری نیست. torch.load از راه ماژول pickle پایتون دیسریالایز میکند و کدِ جاسازیشده در چکپوینت را همان هنگام بارگذاری اجرا میکند — و دلیل مستندِ وجود safetensors دقیقاً همین است: تانسور خام را با یک سرآیند JSON ذخیره میکند و هیچ چیزی اجرا نمیکند. داخل محیط ایزوله نه اسکنر بالادستی دارید و نه فروشندهای که نیمهشب پیجش کنید، پس دروازهٔ فرمت تنها کنترل شماست. همین منطق دربارهٔ trust_remote_code=True در Transformers هم صادق است: آن پرچم پایتونِ همراه وزنها را import میکند. در محیط ایزوله خاموش میماند، و هر مدلی که به آن نیاز دارد باید کد مدلسازیاش بهصورت سورس وارد مخزن شود و در مسیر کند بازبینی گردد.
فهرست تانسورهای یک فایل safetensors را هم میشود بدون تخصیص حتی یک بایت حافظهٔ GPU خواند، که راه ارزانی است برای اطمینان از اینکه معماری همانی است که تأیید کردهاید:
# check_header.py — reads the safetensors header only. torch not required.
import json, struct, sys
with open(sys.argv[1], "rb") as f:
(header_len,) = struct.unpack("<Q", f.read(8))
header = json.loads(f.read(header_len))
tensors = {k: v for k, v in header.items() if k != "__metadata__"}
print(f"{len(tensors)} tensors")
for name in sorted(tensors)[:5]:
t = tensors[name]
print(f" {name:60s} {t['dtype']:8s} {t['shape']}")
آینهکردن رجیستری مدل در داخل مرز
داخل مرز به یک رجیستری نیاز دارید، نه به پوشهای پر از فایل — چون چیزی که قرار است به آن بازگردید باید با نام آدرسپذیر باشد. دو سازوکار بیشتر پشتههای درونسازمانی را پوشش میدهد.
Ollama همهچیز را زیر ~/.ollama/models نگه میدارد: بلابها به شکل blobs/sha256-<hash> و درخت فهرستنامه زیر manifests/<registry>/<namespace>/<model>/<tag>. این چیدمان بر پایهٔ محتوا آدرسدهی میشود (content-addressed)، پس بلابها از پیش تأییدپذیرند و انتقال یک مدل یعنی کپیکردن بلابهایی که یک فهرستنامه به آنها ارجاع میدهد، بههمراه خود فهرستنامه. راه پایدارتر در محیط ایزوله این است که رجیستری بالادستی را بهکل کنار بگذارید و مدل را بهصورت محلی از یک فایل GGUF که خودتان عبور دادهاید بسازید:
# Modelfile — build inside the boundary, tag with the bundle version.
FROM ./qwen2.5-14b-instruct-q4_K_M.gguf
PARAMETER num_ctx 8192
PARAMETER temperature 0.2
SYSTEM """You answer questions about the data you are given. If the data does
not contain the answer, say so."""
ollama create internal/analyst:2026-08-27 -f Modelfile
ollama list # confirm the tag exists before any traffic is pointed at it
برچسبزدن با تاریخ بسته بهجای latest همان چیزی است که بازگردانی را بعداً ممکن میکند. latest نامی است که در هر لحظه فقط به یک چیز اشاره میکند؛ به آن نمیشود بازگشت.
مدلهای با قالب Hugging Face که vLLM یا یک ریزسرویس مبتنی بر Transformers سرویسشان میدهد، به سنجاقشدن حافظهٔ نهان و خاموشکردن تماسهای شبکه نیاز دارند. هر دو تنظیم محیطیاند و هر دو باید در سطح واحد systemd نوشته شوند تا در یک شل فراموش نشوند:
# /etc/systemd/system/embeddings.service.d/offline.conf
[Service]
Environment=HF_HUB_OFFLINE=1
Environment=TRANSFORMERS_OFFLINE=1
Environment=HF_HUB_DISABLE_TELEMETRY=1
Environment=HF_HOME=/srv/models/hf
Environment=VLLM_NO_USAGE_STATS=1
Environment=DO_NOT_TRACK=1
HF_HUB_OFFLINE=1 هنگام بارگذاری مدل جلوی تماسهای HTTP با هاب را میگیرد و در حالت آفلاین تلهمتری هم خاموش میشود (متغیرهای محیطی huggingface_hub). آمار استفادهٔ vLLM با VLLM_DO_NOT_TRACK، DO_NOT_TRACK یا VLLM_NO_USAGE_STATS خاموش میشود، یا با وجود فایل $HOME/.config/vllm/do_not_track (آمار استفادهٔ vLLM). اینها را تنظیم کنید — اما بهعنوان کنترل به آنها تکیه نکنید. در محیط ایزوله، کنترلْ این است که بستهها جایی برای رفتن ندارند؛ این متغیرها برای آن هستند که لاگهایتان پر از خطای اتصال نشود.
مسیر تعبیهسازی (embedding) خودِ ما یک ریزسرویس خودمیزبان BGE-M3 است: بردار چگال ۱۰۲۴-بعدی، بر پایهٔ XLM-RoBERTa، با پروانهٔ MIT. پروانه اینجا پانویس نیست، بخشی از تصمیم استقرار است: مدلی که وزنهایش قابل بازتوزیع نباشد، از نظر حقوقی نمیتواند روی یک حافظهٔ USB بهدست مجری از مرز عبور کند، و همین پیش از هر معیارسنجی بخش بزرگی از فهرست نامزدها را حذف میکند.
انتشار، یک سطر در جدول تنظیمات است، نه یک استقرار
در محیط ایزوله استقرار گران است: یعنی یک بستهٔ دیگر، یک تأیید دیگر، یک رویداد انتقال دیگر. پس مدلِ در حال استفاده نباید قلمِ استقرار باشد. در دیتاکوپایلوت مدل فعال در جدول کلید-مقدارِ app_settings نگهداری میشود و در زمان اجرا سوئیچ میشود؛ یعنی اپراتور مدل را عوض میکند بدون آنکه نسخهای منتشر شود.
همین ویژگی است که انتشار مرحلهای (staged rollout) را در سمت داخل ممکن میکند:
-- Stage 1: the new model exists in the registry but serves no traffic.
INSERT INTO app_settings (key, value)
VALUES ('llm.model.candidate', 'internal/analyst:2026-08-27')
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
-- Stage 2: route one named group to the candidate. Everyone else is untouched.
UPDATE app_settings
SET value = 'internal/analyst:2026-08-27'
WHERE key = 'llm.model.group.data_team';
-- Stage 3: promote, keeping the previous value written down in the same
-- statement so the rollback target is never a matter of memory.
UPDATE app_settings
SET value = 'internal/analyst:2026-08-27'
WHERE key = 'llm.model.default';
هر مرحله را بهعنوان تغییری مستقل اجرا کنید، با پنجرهٔ مشاهدهٔ تعریفشده در فاصلهٔ میان آنها. آنچه در محیط ایزوله میپایید باریکتر از چیزی است که یک تیم متصل میپاید: نه خوراک حوادث بالادستی دارید، نه صفحهٔ وضعیت فروشنده، و نه امکان مقایسه با یک شاهد میزبانیشده. پس چیزهایی را بپایید که محلی و بدون ابهاماند — نرخ امتناع، خطای تجزیهٔ فراخوانی ابزار، توزیع طول خروجی، و نرخی که عامل به سقف گامهایش میخورد.
بازگردانی هم یک بهروزرسانی سطر است، و باید تمرین شده باشد
بازگردانی (rollback) در محیط ایزوله یک شرط دارد: نسخهٔ قبلی مدل باید هنوز روی میزبان حاضر باشد. اگر نقشهٔ بازگردانی «بستهٔ قدیمی را دوباره میبریم داخل» است، نقشهٔ بازگردانی وجود ندارد — چون آن انتقال ساعتها طول میکشد و تأیید لازم دارد.
دستکم نسخهٔ جاری و نسخهٔ پیشین را روی هر میزبان استنتاج نگه دارید، و نگهداشت دیسک را به یک سیاست صریح تبدیل کنید، نه به اثر جانبی ollama rm. آنوقت بازگردانی این است:
UPDATE app_settings SET value = 'internal/analyst:2026-06-14'
WHERE key = 'llm.model.default';
این را در پنجرهٔ انتشار تمرین کنید، نه وسط حادثه. بازگردانیای که هرگز اجرا نشده، فرضیه است نه رویه.
ردی که حسابرس واقعاً میخواهد
حسابرسی که یک محیط ایزوله را بازبینی میکند، دربارهٔ هر قلمی روی آن چهار پرسش دارد، و مدل استثنا نیست: این چیست، از کجا آمده، چه کسی تأییدش کرده، و چه کسی جابهجایش کرده. هر چهار پاسخ را در یک سند بنویسید، همان لحظهٔ انتقال.
| فیلد | منبع | چرا پرسیده میشود |
|---|---|---|
| برچسب بسته و SHA-256 فهرستنامه | MANIFEST.sha256 | هویت — مدل در حال اجرا را به مجموعهٔ مشخصی از بایتها گره میزند |
| مخزن بالادستی و بازبینی کامیت | PROVENANCE.json | خاستگاه، در بازبینیای که بعداً نمیتوان جابهجایش کرد |
| اثر انگشت کلید امضا و زمان تأیید | خروجی gpg --verify | اصالت، و اینکه چه کسی ضمانتش کرده |
| متن پروانه که در بسته حمل شده | LICENSE | حق بازتوزیع برای خودِ انتقال |
| شمارهٔ سریال رسانهٔ انتقال و متصدی آن | سند تغییر | زنجیرهٔ فیزیکی نگهداشت |
| تغییرات جدول تنظیمات با مقدار پیش و پس | ردگیری فعالیت اپلیکیشن | اینکه واقعاً چه چیزی و از چه زمانی سرویس میداده |
یک انتخاب طراحی اینجا ارزش کپیکردن دارد: سند ردگیری را در همان تراکنش پایگاهدادهای بنویسید که تغییر را مینویسد. سامانهٔ پژوهش حقوقی ما همین کار را میکند — سطر ردگیری و خودِ کنش با هم کامیت میشوند، و یک تریگر پایگاهداده باعث میشود UPDATE و DELETE روی جدول ردگیری خطا بدهند. لاگی که بعداً و بهدست فرایندی جداگانه نوشته شود، لاگی است که میتواند با واقعیت اختلاف پیدا کند — و در محیط ایزوله منبع دومی برای تطبیق آن وجود ندارد.
جایی که این توصیه دیگر کار نمیکند
این رویه یک محیط کاملاً بریده از شبکه را فرض میگیرد، با گام انتقال فیزیکی و مرجع امضایی که در اختیار خودتان است. در چهار وضعیت بهدرستی منتقل نمیشود.
اگر پراکسی خروجی تأییدشدهای به آینهٔ یک فروشنده دارید، کار فهرستنامه و امضا همچنان ارزشش را دارد، اما آرایش انتقال مرحلهای سربار است — از راه آینه بکشید و هنگام رسیدن تأیید کنید.
اگر مدلهایتان را یک فروشندهٔ تجاری بهصورت بلاب رمزگذاریشده یا دارای بررسی پروانه تحویل میدهد، دروازهٔ فرمت نمیتواند داخلشان را ببیند و سند تبارنامه به امضای خود فروشنده وابسته میشود. این موقعیت بهطور محسوسی ضعیفتر است و بهتر است پیش از امضای قرارداد بدانید.
اگر زمان اجرا را بهروز میکنید و نه وزنها را، این رویهٔ درستی نیست. بهروزرسانی زمان اجرا سطح تماس CUDA و درایور را عوض میکند و به محیط تمرینی با همان مدل GPU نیاز دارد، نه به یک مجموع کنترلی و یک سوئیچ در جدول تنظیمات.
و اگر محیط ایزولهٔ شما میزبان استنتاج دوم ندارد، هیچکدام از توصیههای انتشار مرحلهای زنده نمیماند: با یک میزبان، ارتقا و بازگردانی یک رویداد واحدند، و تنها کاهش خطر واقعیِ باقیمانده این است که مدل پیشین را حاضر نگه دارید و فرمان بازگردانی را جایی بنویسید که اپراتور کشیک، ساعت سه بامداد، پیدایش کند.