Skip to content
गाइड14 मिनट में पढ़ें

सर्वर पर Potato की तैनाती

Potato को प्रोडक्शन में nginx और TLS के पीछे, Docker में या VM पर चलाएँ, और जानें कि Potato सर्वर कई workers के बजाय एक ही प्रोसेस के रूप में क्यों चलता है।

Potato Team

हर प्रोजेक्ट के लिए एक Potato प्रोसेस चलाएँ, उसके आगे nginx और एक TLS सर्टिफ़िकेट रखें, और ज़्यादा concurrency चाहिए तो worker प्रोसेस के बजाय threads बढ़ाएँ। दूसरा worker प्रोसेस बिना कोई चेतावनी दिए एनोटेशन खो देता है।

Potato अपना आइटम पूल, असाइनमेंट कतार और हर एनोटेटर की स्थिति सर्वर प्रोसेस के भीतर मेमोरी में रखता है। यही एक तथ्य तैनाती के ज़्यादातर फ़ैसले तय करता है। इसी इन-प्रोसेस स्थिति के कारण Potato सर्वर workers से नहीं बल्कि threads से स्केल करता है, एनोटेशन आउटपुट डायरेक्टरी वह चीज़ है जिसका बैकअप लेना है, और कई Potato प्रोसेस के बीच लोड बैलेंसिंग किसी अध्ययन को तेज़ करने के बजाय उसे बिगाड़ देती है। यह मार्गदर्शिका सर्वर चलाने के तीन तरीके, उसके आगे का reverse proxy और TLS, और वे कॉन्फ़िग keys बताती है जो असली एनोटेटर के काम शुरू करने पर मायने रखती हैं।

Potato सर्वर एक ही प्रोसेस के रूप में क्यों चलता है

gunicorn के दूसरे worker को आइटम पूल, असाइनमेंट कतार और यूज़र स्थिति की अपनी अलग कॉपी मिलती है। वह ऐसे instances बाँटता है जो पहला worker पहले ही असाइन कर चुका है, और क्योंकि हर सेव पर user_state.json पूरा का पूरा दोबारा लिखा जाता है, जो worker आख़िर में सेव करता है वह दूसरे worker के एनोटेशन मिटा देता है। लॉग में इसकी कोई सूचना नहीं आती। एनोटेशन गायब हो जाते हैं, और पहला संकेत आमतौर पर किसी एनोटेटर का यह कहना होता है कि उसने एक आइटम लेबल किया था जो अब अछूता दिखता है।

Potato की प्रकाशित Docker इमेज इस नियम को लागू करती है। वह gunicorn का एक ही worker चलाती है और इससे ज़्यादा के साथ शुरू होने से मना कर देती है, और POTATO_ALLOW_MULTIWORKER=1 उन सब मामलों के लिए यह रोक हटा देता है जो असली अध्ययन नहीं हैं। concurrency इसके बजाय threads से आती है, जो स्थिति की एक ही कॉपी साझा करते हैं। इमेज GUNICORN_THREADS के ज़रिए डिफ़ॉल्ट रूप से आठ threads इस्तेमाल करती है, जो एक सामान्य अध्ययन के दर्जनों एक साथ काम करने वाले एनोटेटरों के लिए काफ़ी है।

यही बाध्यता इस मार्गदर्शिका के बाकी हिस्से की सीमा तय करती है। हर प्रोजेक्ट के लिए एक प्रोसेस चलाएँ, उसे डेटासेट के लिए पर्याप्त मेमोरी दें, और reverse proxy का इस्तेमाल TLS और स्टैटिक फ़ाइलों के लिए करें, लोड बाँटने के लिए नहीं।

सर्वर चलाने के तीन तरीके

तरीकाइससे क्या मिलता हैकब इस्तेमाल करें
potato deployआपके कॉन्फ़िग से होस्ट, TLS और इमेज तैयार करता हैऐसा अध्ययन जिसे आप आज ही चालू करना चाहते हैं
Docker इमेजहर बार वही bytes, dependencies बिल्ड करने की ज़रूरत नहींऐसी मशीन जिसे आप पहले से संभालते हैं
VM और systemdहोस्ट और Python एनवायरनमेंट पर पूरा नियंत्रणअपने नियमों वाला संस्थागत सर्वर

तेज़ रास्ते के लिए potato deploy

potato deploy एक होस्ट तैयार करता है, इमेज इंस्टॉल करता है और आपकी मौजूदा कॉन्फ़िग फ़ाइल से TLS सेट करता है:

bash
potato deploy up myproject/config.yaml --provider digitalocean

कुछ मिनट बाद आपके पास एनोटेटरों को भेजने के लिए एक HTTPS URL होता है, और potato deploy pull एनोटेशन वापस ले आता है। 2.10.0 से टारगेट हैं local, potato share और तेरह क्लाउड होस्ट, जिनमें AWS, Jetstream2, Hetzner, Fly और Railway शामिल हैं, और इनमें फ़र्क यह है कि रीस्टार्ट के बाद डेटा बचता है या नहीं। सात VM टारगेट में persistent डिस्क, लॉग स्ट्रीमिंग और SSH तीनों एक साथ हैं, और जो होस्ट अपनी डिस्क मिटा देता है उसे --backup चाहिए, जो एनोटेशन को HuggingFace डेटासेट या S3 बकेट में कॉपी करता है। Potato को लोकल या क्लाउड में चलाना इनकी तुलना करता है। शुरुआत local से करें, जो वही bundle और वही इमेज बिना किसी खाते और बिना बिल के चलाता है, और कुछ भी ख़र्च होने से पहले बता देता है कि क्या तैनात होगा।

प्रकाशित Docker इमेज

Potato GitHub Container Registry पर एक पहले से बनी इमेज प्रकाशित करता है। इसमें सर्वर और उसकी dependencies होती हैं, और आपका प्रोजेक्ट रनटाइम पर /app पर माउंट होता है, इसलिए एक ही इमेज हर टास्क के काम आती है:

bash
docker run -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  -e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
  ghcr.io/davidjurgens/potato:latest

यह इमेज इसलिए है क्योंकि Potato की मुख्य dependencies में numpy, pandas, scipy और scikit-learn शामिल हैं। इन्हें छोटे VM पर इंस्टॉल करना अच्छी स्थिति में भी धीमा है। 1 GB की मशीन पर मेमोरी ख़त्म होने से बिल्ड मार दिया जाता है, और 2 GB पर हर दोबारा तैनाती में पाँच से दस मिनट लगते हैं। इमेज pull करने में एक मिनट से कम लगता है।

दो टैग मायने रखते हैं। latest में मुख्य dependencies हैं और इसका आकार लगभग 840 MB है, और latest-all इसमें AI SDKs, डॉक्यूमेंट फ़ॉर्मेट, एक्सपोर्ट और OAuth लॉगिन जोड़ता है, जिससे आकार लगभग 1.5 GB हो जाता है। दोनों linux/amd64 और linux/arm64 के लिए बनाए जाते हैं। महीनों चलने वाले अध्ययन के लिए latest के बजाय एक वर्ज़न पिन करें, क्योंकि latest आपकी जानकारी के बिना बदल जाता है।

जो भी चीज़ एक रन से ज़्यादा चलनी है, उस पर POTATO_SECRET_KEY सेट करें। इसके बिना Flask सेशन साइन करने वाली key हर प्रोसेस के लिए रैंडम होती है, इसलिए हर रीस्टार्ट सभी एनोटेटरों को लॉग आउट कर देता है। POTATO_CONFIG कंटेनर के भीतर कॉन्फ़िग का पाथ चुनता है, PORT सुनने वाला पोर्ट सेट करता है (डिफ़ॉल्ट 7860), और GUNICORN_TIMEOUT worker का टाइमआउट सेकंड में सेट करता है।

इमेज uid 1000 के रूप में चलती है, और bind mount होस्ट का ownership बनाए रखता है, इसलिए Linux पर कंटेनर अक्सर आपकी बनाई डायरेक्टरी में लिख नहीं पाता। सर्वर शुरू होते समय इसकी जाँच करता है और बूट के बीच में क्रैश होने के बजाय चलने से मना कर देता है। या तो sudo chown -R 1000:1000 myproject से डायरेक्टरी कंटेनर यूज़र को सौंप दें, या docker run --user "$(id -u):$(id -g)" से ख़ुद अपने यूज़र के रूप में चलाएँ, जिससे एनोटेशन भी आपके ownership में रहते हैं। macOS और Windows पर Docker Desktop bind mounts पर ownership को नज़रअंदाज़ करता है, इसलिए जो सेटअप Mac पर चलता है वह Linux सर्वर पर फिर भी फ़ेल हो सकता है।

VM पर इंस्टॉल करना

जिस मशीन को आप सीधे संभालते हैं, उस पर Potato को एक virtual environment में इंस्टॉल करें और प्रोसेस की ज़िम्मेदारी systemd को दें।

bash
sudo apt update
sudo apt install python3 python3-venv python3-pip nginx certbot python3-certbot-nginx
 
sudo mkdir -p /opt/potato/projects
cd /opt/potato
python3 -m venv venv
source venv/bin/activate
pip install potato-annotation gunicorn
potato --version

ऐप को gunicorn के तहत एक worker और कई threads के साथ चलाएँ। application factory potato.flask_server:create_app में है और कॉन्फ़िग का पाथ लेती है:

bash
gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
  --chdir /opt/potato/projects/my_project \
  "potato.flask_server:create_app('config.yaml')"

/etc/systemd/system/potato.service बनाएँ ताकि रीबूट या क्रैश के बाद सर्वर फिर से चालू हो जाए:

ini
[Unit]
Description=Potato Annotation Server
After=network.target
 
[Service]
Type=simple
User=potato
Group=potato
WorkingDirectory=/opt/potato/projects/my_project
Environment="PATH=/opt/potato/venv/bin"
EnvironmentFile=/opt/potato/.env
ExecStart=/opt/potato/venv/bin/gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
  "potato.flask_server:create_app('config.yaml')"
Restart=always
RestartSec=10
 
[Install]
WantedBy=multi-user.target

इसे enable करें और शुरू करें:

bash
sudo systemctl enable potato
sudo systemctl start potato
sudo systemctl status potato

EnvironmentFile में वे secrets रहते हैं जो कॉन्फ़िग में नहीं होने चाहिए:

bash
# /opt/potato/.env
POTATO_SECRET_KEY=a-long-random-value
OPENAI_API_KEY=sk-...

secret key एक बार openssl rand -hex 32 से बनाएँ और पूरे अध्ययन के दौरान वही रखें। इसे बदलने से सभी सेशन अमान्य हो जाते हैं और सभी एनोटेटर लॉग आउट हो जाते हैं।

nginx और TLS

Potato localhost से bind होता है और nginx उसके आगे TLS terminate करता है। इमेज और ऑडियो प्रोजेक्ट में अपलोड सीमा मायने रखती है, जहाँ 1 MB की डिफ़ॉल्ट body सीमा मीडिया को अस्वीकार कर देती है और एनोटेटर को यह विफलता दिखती भी नहीं।

nginx
server {
    listen 443 ssl;
    http2 on;
    server_name annotation.example.com;
 
    ssl_certificate /etc/letsencrypt/live/annotation.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/annotation.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
 
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
 
    client_max_body_size 100M;
 
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

साइट enable करें और सर्टिफ़िकेट लें:

bash
sudo ln -s /etc/nginx/sites-available/potato /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d annotation.example.com
sudo systemctl enable certbot.timer

फ़ायरवॉल पर बाकी सब बंद कर दें, क्योंकि आगे proxy लग जाने के बाद Potato का पोर्ट सीधे पहुँच में नहीं होना चाहिए:

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

एक होस्टनेम के तहत कई प्रोजेक्ट चलाना

संस्थान अक्सर पाथ को लोकल पोर्ट से मैप करके कई Potato सर्वरों को एक ही सार्वजनिक HTTPS endpoint से उपलब्ध कराते हैं। एनोटेशन UI assets माँगने और सेव करने के लिए root-relative URLs का इस्तेमाल करता है, इसलिए /app1 पर माउंट किए गए सर्वर को अपना prefix पता होना चाहिए, वरना CSS 404 देता है और autosave "annotations not saved" के साथ फ़ेल हो जाता है। Potato prefix को WSGI के SCRIPT_NAME से पढ़ता है, और इसे सेट करने के दो तरीके हैं।

जब proxy आपके नियंत्रण में हो और वह X-Forwarded-Prefix भेज सके, तब POTATO_PROXY_FIX=1 इस्तेमाल करें। जब proxy का कॉन्फ़िगरेशन बदलना आपके हाथ में न हो, तब POTATO_URL_PREFIX=/app1 इस्तेमाल करें। proxy को फ़ॉरवर्ड करने से पहले prefix हटाना फिर भी ज़रूरी है, ताकि Flask को बिना prefix वाले पाथ मिलते रहें:

nginx
location /app1/ {
    proxy_pass         http://127.0.0.1:8000/;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Prefix /app1;
}

ProxyFix फ़ॉरवर्ड किए गए headers पर भरोसा करता है, इसलिए POTATO_PROXY_FIX तभी चालू करें जब ऐप सिर्फ़ भरोसेमंद proxy के ज़रिए ही पहुँच में हो। अगर अंदरूनी पोर्ट भी सीधे पहुँच में है, तो कोई क्लाइंट prefix को नकली बनाकर Potato के बनाए URLs को दूषित कर सकता है।

प्रोडक्शन प्रोजेक्ट को किस कॉन्फ़िग की ज़रूरत है

चार सेटिंग्स प्रोडक्शन कॉन्फ़िग को लोकल कॉन्फ़िग से अलग करती हैं: एक स्थिर signing key, पासवर्ड की अनिवार्यता, स्पष्ट सेशन अवधि, और एक आउटपुट डायरेक्टरी जिसका आपके पास बैकअप हो।

yaml
annotation_task_name: "Production Annotation Task"
task_description: "Classify each passage."
 
host: 127.0.0.1
port: 8000
 
# Sessions survive a restart only when the signing key is stable.
# Set it here, or leave it out and supply POTATO_SECRET_KEY in the environment.
require_password: true
persist_sessions: true
session_timeout_minutes: 480
 
data_files:
  - data/items.jsonl
 
item_properties:
  id_key: id
  text_key: text
 
output_annotation_dir: annotation_output/production/
export_annotation_format: jsonl
 
user_config:
  allow_all_users: false
  users:
    - annotator1
    - annotator2
 
annotation_schemes:
  - annotation_type: radio
    name: sentiment
    description: "What is the sentiment expressed in this text?"
    labels:
      - name: positive
      - name: neutral
      - name: negative
    label_requirement:
      required: true

session_timeout_minutes निष्क्रिय समय की सीमा तय करता है, शिफ़्ट की लंबाई की नहीं, क्योंकि हर अनुरोध पर घड़ी फिर से शुरू होती है। 480 का डिफ़ॉल्ट पूरे दिन काम करने वाले एनोटेटरों के लिए ठीक है; मशीन साझा हो तो इसे कम करें। जब भी सर्वर सार्वजनिक इंटरनेट से पहुँच में हो, allow_all_users: false सेट करें और अपने एनोटेटरों की सूची दें, ताकि URL ढूँढ लेने वाला कोई अजनबी रजिस्टर करके आपके डेटासेट में लिखना शुरू न कर सके।

हेल्थ चेक और लॉग

/health कंटेनरों और लोड बैलेंसरों के लिए बिना authentication वाला liveness probe है, और यह जानबूझकर लगभग कुछ नहीं बताता, क्योंकि यह जो भी कहता है वह सार्वजनिक है:

bash
curl http://localhost:8000/health
# {"status": "ok"}          once the state managers exist
# {"status": "starting"}    503 while the server is still coming up

कंटेनर का healthcheck और तैनाती प्रदाता की निर्माण के बाद की polling, दोनों इसी बदलाव पर निर्भर हैं। टास्क के नाम, आइटम की संख्या और एनोटेटरों की संख्या इसके बजाय /admin/health से मिलती हैं, जिसके लिए admin API key चाहिए।

लॉगिंग प्रोसेस के ज़रिए होती है, इसलिए systemd उसे कैप्चर करता है:

bash
sudo journalctl -u potato -f

कॉन्फ़िग में verbose: true से स्तर बढ़ाएँ, या debug_log से debug लॉगिंग के वे हिस्से चालू करें जिनकी आपको ज़रूरत है; यह all, ui, server या none लेता है। असली अध्ययन के दौरान दोनों बंद रखें, क्योंकि debug लॉगिंग इतनी विस्तृत होती है कि लंबे रन में छोटी डिस्क भर सकती है।

किसका बैकअप लें

प्रोजेक्ट डायरेक्टरी की तीन चीज़ें दोबारा नहीं बनाई जा सकतीं। output_annotation_dir के तहत एनोटेशन आउटपुट ही अध्ययन है। project.sqlite में memos, codebook, cases, सर्च इंडेक्स और review workflow रहते हैं। datasets.sqlite में डेटासेट के रिकॉर्ड रहते हैं। बाकी सब कुछ आपकी डेटा फ़ाइलों और कॉन्फ़िग से दोबारा बनाया जा सकता है।

bash
#!/bin/bash
# /opt/potato/backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/opt/potato/backups
 
tar -czf $BACKUP_DIR/annotations_$DATE.tar.gz \
  /opt/potato/projects/*/annotation_output/ \
  /opt/potato/projects/*/project.sqlite \
  /opt/potato/projects/*/datasets.sqlite \
  /opt/potato/projects/*/config.yaml
 
find $BACKUP_DIR -mtime +7 -delete

इसे हर रात crontab से चलाएँ, और आर्काइव को मशीन से बाहर कॉपी करें:

bash
0 2 * * * /opt/potato/backup.sh

मशीन का आकार तय करना

मेमोरी वह संसाधन है जो डेटासेट के साथ बढ़ता है, क्योंकि आइटम पूल प्रोसेस में रखा जाता है। बाकी सब कुछ lazy बना दिया गया है, इसलिए embedding और sentence-transformers स्टैक बूट के समय तब तक इंपोर्ट नहीं होता जब तक किसी फ़ीचर को उसकी ज़रूरत न हो। v2.6.0 के रिलीज़ नोट्स के संदर्भ डेटासेट पर resident मेमोरी लगभग 750 MB से घटकर लगभग 365 MB रह गई, और 50,000 आइटम वाला बूट लगभग 10 सेकंड से घटकर लगभग 5.7 सेकंड हो गया।

प्रति आइटम लागत छोटी है और मापी जा सकती है। एक इमेज URL और दो फ़ील्ड वाला vision आइटम लगभग 932 bytes लेता है, और लगभग 540 अद्वितीय अक्षरों वाला टेक्स्ट आइटम लगभग 1430 bytes। इसलिए दस लाख टेक्स्ट आइटम लगभग 1.43 GB resident मेमोरी के बराबर हैं, और बजट इसी संख्या पर बनाना चाहिए।

जिन corpora में यह मायने रखता है, उनमें आइटम payloads एक SQLite फ़ाइल में एक छोटे इन-मेमोरी cache के साथ रखे जा सकते हैं:

yaml
item_store:
  backend: paged
  cache_size: 2048

यह ब्लॉक ऊपर वाले कॉन्फ़िग में जोड़ें। इसे चालू करने से पहले माप पढ़ लें। paging से vision आइटम पर 28% और टेक्स्ट पर 51% की बचत होती है, दस गुना की नहीं, और बदले में corpus लोड होने में लगभग 2.5 गुना समय लगता है। दस लाख से कम आइटम पर इससे बस एक धीमा सर्वर मिलता है और कोई फ़ायदा नहीं।

किसी प्रोजेक्ट को कई सर्वरों में बाँटने के बजाय ज़्यादा RAM वाली मशीन चुनें, क्योंकि बाँटना वही multi-process विफलता है जिसका वर्णन इस मार्गदर्शिका की शुरुआत में है। जब एक मशीन सचमुच corpus को नहीं सँभाल सकती, तब इसके बजाय काम को बाँटें। batch assignment हर एनोटेटर को एक हिस्सा देता है, per_annotator_quota तय करता है कि किसी एक व्यक्ति को अधिकतम कितना असाइन हो, और directory watching के साथ data_directory आपको सर्वर चलते हुए फ़ाइलें जोड़ने देता है।

समस्या निवारण

पोर्ट पहले से इस्तेमाल में है। उसी प्रोजेक्ट डायरेक्टरी पर दूसरे पोर्ट पर दूसरा Potato शुरू करने के बजाय प्रोसेस ढूँढें और उसे बंद करें:

bash
sudo lsof -i :8000

प्रोजेक्ट डायरेक्टरी पर permission denied। सर्विस यूज़र को उसका मालिक होना चाहिए, और Docker में वह यूज़र uid 1000 है:

bash
sudo chown -R potato:potato /opt/potato

हर रीस्टार्ट के बाद एनोटेटर लॉग आउट हो जाते हैं। POTATO_SECRET_KEY या कॉन्फ़िग की secret_key वैल्यू मौजूद नहीं है, इसलिए हर बूट पर signing key नई बनती है।

proxy के पीछे assets 404 देते हैं और autosave फ़ेल होते हैं। सर्वर को अपना URL prefix पता नहीं है। POTATO_PROXY_FIX=1 या POTATO_URL_PREFIX सेट करें, और जाँचें कि proxy फ़ॉरवर्ड करने से पहले prefix हटाता है।

आगे पढ़ें