ArtStation: ایک ہفتے میں خالی ریپو سے ٹیسٹ شدہ، ملٹی GPU ٹیکسچر پائپ لائن تک
QuickTile Studio کسی material کو seamless ٹائل میں بدلتا ہے۔ ٹیکسچرز بیچنے کے لیے خود materials چاہییں، بڑی تعداد میں اور یکساں اسٹائلز میں۔ فروری کے آخری ہفتے میں میں نے ArtStation بنائی، ایک نجی پائپ لائن جو میرے اپنے GPUs پر ٹائل ہونے والے ٹیکسچرز بناتی ہے، اور جس کا ہدف Unity Asset Store کے لیے ٹیکسچر پیکس ہیں۔
ریپوزٹری 22 فروری کو بنی۔ 28 فروری کے اختتام تک:
| کمٹس | 38، ابتدائی کمٹ سمیت |
| ورژن | 27 فروری کو v0.2.0، اسی دن v0.2.2 |
| Prompt تعریفیں | 1,188 YAML فائلیں: 9 اسٹائلز میں سے ہر ایک میں 132 ٹیکسچر اقسام |
| کام کرنے والے مراحل | بنیادی generation، img2img بہتری، تصاویر کی captioning، LoRA ٹریننگ |
| ابھی خالی ڈھانچہ | combinatorial حتمی generation؛ Unity material کی تیاری شروع نہیں ہوئی |
| ریپو میں ٹیسٹ کیسز | 502 pytest، 204 vitest، 38 Playwright |
| GPU شیڈولنگ | ہر Stable Diffusion سرور instance کے لیے ایک worker؛ مرکزی مشین پر دو GPUs |
ٹیسٹ کے اعداد کمٹ شدہ سویٹ میں ٹیسٹ فنکشنز کی گنتی ہیں، کسی رن کا نتیجہ نہیں۔ اور یہ تحریر پائپ لائن کے بارے میں ہے، اس کے آؤٹ پٹ کے بارے میں نہیں: بنائی گئی تصاویر جان بوجھ کر کمٹ نہیں کی جاتیں، اس لیے ریپوزٹری میں اس کا کوئی ریکارڈ نہیں کہ اس نے کتنی تصاویر بنائیں، اور میں کوئی اندازہ پیش نہیں کروں گا۔
زیادہ تر کوڈ AI کوڈنگ ایجنٹس نے میری ہدایت پر لکھا (38 میں سے 17 کمٹس پر ایجنٹ کی co-author لائن موجود ہے)۔ آرکیٹیکچر میں نے طے کیا، تقاضے میں نے لکھے، اور جو کچھ شامل ہوا اس کا جائزہ میں نے لیا۔
ڈھانچہ: فولڈرز بطور معاہدہ
ریپوزٹری بننے سے پہلے یہ ورک فلو ہاتھ سے ثابت کیا جا چکا تھا۔ README میں درج ہے کہ Stylized اسٹائل میں 12 prompt تعریفیں ایک تربیت یافتہ اسٹائل LoRA کے ساتھ جانچی گئیں۔ ArtStation اس چکر کو خودکار بناتی ہے۔ ہر مرحلہ پچھلے مرحلے کا فولڈر پڑھتا ہے اور اپنا فولڈر لکھتا ہے:
Prompts/<Style>/<type>.yaml
-> Step 1 txt2img, tiling=true Generated/Step1-Base/<Style>/<Type>/
-> Step 2 img2img, denoise ~0.45 Generated/Step2-Denoised/<Style>/<Type>/
-> Annotate vision-model captions .txt beside each training image
-> Step 3 LoRA training (kohya_ss) trained .safetensors
-> Step 4 LoRA-enhanced final set (stub at month end)
-> Step 5 PBR maps, Unity materials (planned)
Generation ایک مقامی SDNext سرور کی REST API کے ذریعے ہوتی ہے۔ Captioning کے لیے Ollama کے ذریعے Moondream vision ماڈل استعمال ہوتا ہے۔ ٹریننگ کے لیے kohya_ss کو الگ process کے طور پر چلایا جاتا ہے، کیونکہ SDNext اپنی API پر ٹریننگ فراہم نہیں کرتا۔
فولڈرز کو انٹرفیس بنانے کا مطلب تھا کہ ہر مرحلہ الگ سے دوبارہ چلایا، جانچا یا بدلا جا سکتا تھا، اور بعد میں شامل ہونے والی ویب UI کو کسی نئے معاہدے کی ضرورت نہیں پڑی: وہ اسی ڈائریکٹری درخت کو براؤز کرتی ہے۔
ہر تصویر اپنی درخواست ساتھ رکھتی ہے
پہلا ورژن ہر تصویر کے ساتھ prompt کی ایک .txt فائل لکھتا تھا۔ 23 فروری کو یہ بدلا: اب
مرحلہ 1 اور 2 ہر تصویر کے ساتھ ایک .meta.json لکھتے ہیں جس کے تین حصے ہیں:
request: SDNext کو بھیجا گیا مکمل API payloadresponse: generation کی پارس شدہ معلومات (seed، sampler، ماڈل)meta: اسٹائل، ٹیکسچر کی قسم، run ID، ٹائم اسٹیمپ
دو تفصیلات اہم تھیں۔ img2img کی ان پٹ تصویر محفوظ کیے گئے payload سے نکال دی جاتی ہے،
کیونکہ اس کی base64 کاپی ہر ساتھی فائل کو کئی میگا بائٹ کا بنا دیتی۔ اور .txt فائلیں
صرف ایک مقصد کے لیے مخصوص ہو گئیں، یعنی LoRA ٹریننگ کی captions، کیونکہ kohya_ss
captions تصویر کے ہم نام .txt فائلوں سے پڑھتا ہے۔ ایک ہی فائل ایکسٹینشن کے دو معنی
ہوتے تو prompts بطور captions ٹریننگ میں چلے جاتے۔
Caption کا فارمیٹ مقرر ہے تاکہ ٹریننگ اس پر بھروسہ کر سکے:
<trigger_word>, <texture type>, top-down terrain, <Moondream description>
پہلے تین tokens مستقل ہیں، جو ٹریننگ کنفیگ میں keep_tokens=3 سے مطابقت رکھتے ہیں۔
کنفیگریشن بطور resolution chain
ہر قدر چار پرتوں سے گزر کر طے ہوتی ہے، آخری پرت غالب رہتی ہے:
pipeline defaults -> style config.yaml -> preset -> CLI flags
Presets نام والے override مجموعے ہیں (fast-iteration، high-quality،
no-tile-explore، اور ایک ماڈل ٹیسٹ preset)۔ ہر مرحلے کی کمانڈ --preset اور
--override قبول کرتی ہے۔
اس کے اوپر، pipeline.yaml میں host name کے حساب سے ہر مشین کا الگ حصہ ہے جو لوڈ کے
وقت deep-merge ہوتا ہے۔ ایک ہی checkout دو ایسی مشینوں پر چلتا ہے جن کی ڈرائیو ترتیب اور
GPUs کی تعداد مختلف ہے، بغیر کسی branch یا مقامی تبدیلی کے۔
تین ماڈل سرورز کے درمیان GPUs کی شراکت
تین ماڈل سرورز ایک ہی میموری کے لیے مقابلہ کرتے ہیں: SDNext میں SDXL، Ollama میں Moondream، اور ٹریننگ کے دوران kohya_ss۔ فیصلے یہ تھے:
- ہر caption درخواست کے بعد ان لوڈ۔ Ollama سروس کا keep-alive صفر پر کنفیگر ہے، اس لیے caption ماڈل SDXL کے ساتھ میموری میں نہیں ٹھہرتا۔
- Checkpoint ان لوڈ کرنا شامل کیا گیا، پھر واپس لیا گیا۔ 23 فروری کو مرحلہ 1 اور 2
مکمل ہونے پر SDXL checkpoint ان لوڈ کرنے لگے۔ پانچ دن بعد یہ کالز اسی کمٹ میں ہٹا دی
گئیں جس نے
set_modelکو سکھایا کہ اگر مطلوبہ checkpoint پہلے سے لوڈ ہو تو دوبارہ لوڈ نہ کرے۔ اس سودے بازی کو میں یوں سمجھتا ہوں: جب ہر GPU کا اپنا SDNext instance ہو تو لوڈ شدہ checkpoint ہی cache ہے، اور ہر کام کے لیے اسے دوبارہ لوڈ کرنا قیمت ہے۔ کون سا طریقہ درست ہے اس کا انحصار اس پر ہے کہ GPU مشترک ہے یا نہیں؛ ہر GPU کے لیے الگ instance والا سیٹ اپ اسی دن تقریباً ایک گھنٹے بعد شامل ہوا۔ - ہر GPU کے لیے ایک worker۔ Job queue ہر SDNext instance کے لیے ایک async worker چلاتی ہے۔ ہر worker اپنی blocking HTTP کالز ایک thread pool پر چلاتا ہے جس کا سائز instances کی تعداد کے برابر ہے، اس لیے لمبی generation کبھی FastAPI event loop کو نہیں روکتی۔ کسی کام کو منسوخ کرنے پر interrupt صرف اسی instance کو بھیجا جاتا ہے جو وہ کام چلا رہا ہو، سب کو نہیں۔
- نظر رکھنا۔
nvidia-smiکے snapshots (VRAM، utilization، درجۂ حرارت) پائپ لائن کے checkpoints پر لاگ ہوتے ہیں اور ڈیش بورڈ پر ہر GPU کے لیے دکھائے جاتے ہیں۔
ایسی errors جو اپنی قسم بتائیں
دوسرے دن پائپ لائن کو structured logging اور errors کی ایک درجہ بندی ملی:
class PipelineError(Exception): ...
class NetworkError(PipelineError): ...
class ConfigError(PipelineError): ...
class FileSystemError(PipelineError): ...
class TrainingError(PipelineError): ...
class OllamaError(PipelineError): ...
SDNext کلائنٹ میں ہر raise_for_status() کو NetworkError میں لپیٹا جاتا ہے۔ ہر رن
کے لیے ایک run ID بنتی ہے جو تمام مراحل کی ہر لاگ لائن میں شامل ہوتی ہے، اور ہر سروس
(پائپ لائن، SDNext، kohya_ss) کی اپنی rotating لاگ فائل ہے۔ جب کوئی batch رن گھنٹوں بعد
ناکام ہو تو پہلا سوال یہ ہوتا ہے کہ یہ کس قسم کی ناکامی تھی اور کس رن سے تعلق رکھتی تھی۔
اب دونوں جواب صرف ایک لاگ فلٹر ہیں۔
ٹرمینلز نہیں، سروسز کے طور پر چلانا
جو پائپ لائن صرف ٹرمینل کھلا رہنے تک چلے وہ انفراسٹرکچر نہیں۔ 27 فروری کو اجزاء NSSM کے ذریعے انسٹال ہونے والی Windows سروسز بن گئے، ایک ایسے installer کے ساتھ جو:
- سروس کے arguments کو executable کے path سے الگ دیتا ہے، تاکہ spaces والے install paths کام کریں
- hosts فائل میں اضافے سے پہلے اس صورت سے بچاؤ کرتا ہے جب فائل کے آخر میں نئی لائن نہ ہو
- کسی سروس کی ناکامی پر باقی کے ساتھ جاری رہتا ہے، ہر سروس کی کامیابی اور ناکامی کا خلاصہ دکھاتا ہے، اور کچھ بھی ناکام ہو تو non-zero کے ساتھ ختم ہوتا ہے
- Python executable کو hard-code کرنے کی بجائے خود تلاش کرتا ہے
اس کمٹ نے installer کے 29 یونٹ ٹیسٹ شامل کیے۔ ورژن نمبر Python پیکیج میں ایک ہی ماخذ پر منتقل ہو گیا؛ ویب frontend کی ورژن فائل اسی سے بنتی ہے، اور ہر سروس ری اسٹارٹ پر frontend دوبارہ build ہوتا ہے، اس لیے UI پرانا ورژن نہیں دکھا سکتی۔
مارکیٹ سے درجہ بندی
ٹیکسچر پیک تصاویر کا مجموعہ بننے سے پہلے ایک درجہ بندی ہوتا ہے۔ اپنی درجہ بندی کو پہلے سے بکنے والی چیزوں سے ملانے کے لیے میں نے ایک Playwright scraper شامل کیا جو Asset Store پیکیج صفحات سے content tree نکالتا ہے۔ یہ صفحات virtualized فہرست استعمال کرتے ہیں، اس لیے یہ درخت پڑھنے سے پہلے ہر فولڈر نوڈ کو کھولتا ہے اور rendering پر مجبور کرنے کے لیے container کو اسکرول کرتا ہے۔ اس نے موازنے کے لیے تین بڑے مسابقتی ٹیکسچر مجموعوں کے content trees حاصل کیے، اور اسی کمٹ نے Fabric and Textile (کپڑا اور ٹیکسٹائل) کی کیٹیگری شامل کی۔ اس کے بعد Stylized کی 132 ٹیکسچر اقسام کو آٹھ دیگر اسٹائل فولڈرز میں ان کے نقطۂ آغاز کے طور پر کاپی کیا گیا۔
پہلے ٹیسٹ، پھر قرض کی فہرست
28 فروری کو پروجیکٹ کو تینوں پرتوں پر محیط ٹیسٹ سویٹ ملا: پائپ لائن اور API کے لیے
pytest، Vue frontend کے لیے vitest، اور Playwright end-to-end ٹیسٹ جو page.route()
سے API کی نقل کرتے ہیں۔ ایک ہی runner اسکرپٹ تینوں چلاتا ہے اور HTML رپورٹ لکھتا ہے۔
ایک اور قابلِ قدر عادت ایک known-issues.md تھی جو تکنیکی قرض کو ٹھیک ٹھیک نام دیتی ہے۔
ایک اندراج: کوڈ combination کلید پڑھتا تھا جبکہ ہر YAML فائل combinations استعمال
کرتی تھی، اس لیے یہ سیٹنگ خاموشی سے ہمیشہ خالی رہتی اور ڈیفالٹس پر چلی جاتی۔ اب loader
دونوں قبول کرتا ہے۔ دیگر اندراجات ایک ایسی README schema کی نشاندہی کرتے ہیں جو اصل فائلوں
کی بجائے ہدف ڈیزائن بیان کرتی تھی (اب اسے ہدف کے طور پر واضح کیا گیا ہے)، اور ایسے ڈائریکٹری
constants کی جنہیں کوئی استعمال نہیں کرتا تھا۔ انہیں لکھ دینے کا مطلب تھا کہ بعد میں کوڈ
سنبھالنے والا ایجنٹ غلط ماخذ پر بھروسہ نہیں کرے گا۔
جو بات عمومی طور پر لاگو ہوتی ہے
ہر آؤٹ پٹ کو اس کے ان پٹ ساتھ رکھنے دیں۔ درست درخواست والی ایک ساتھی فائل کسی بھی تصویر کو دہرائے جا سکنے والے تجربے میں بدل دیتی ہے، اور اس کی قیمت صرف ایک فائل لکھنا ہے۔
ہر فائل کی قسم کو ایک ہی معنی دیں۔ Prompts اور ٹریننگ captions دونوں .txt چاہتے
تھے۔ ٹریننگ چلنے سے پہلے انہیں الگ کرنے سے ایک خاموش ڈیٹا بگ سے بچاؤ ہوا۔
VRAM پالیسی ترتیب (topology) پر منحصر ہے۔ جب ماڈلز ایک GPU بانٹتے ہوں تو ہر مرحلے کے بعد ان لوڈ کرنا درست ہے، اور جب ہر GPU کا اپنا مخصوص سرور ہو تو غلط۔ میں نے پہلا انتخاب اسی دن واپس لیا جس دن ہر GPU والی ترتیب شامل ہوئی۔
تکنیکی قرض کو کوڈ جتنی درستگی سے لکھیں۔ ایک known-issues فائل جو بتائے کہ کیا غلط ہے، کہاں اور کیوں، وہی اگلی تبدیلی کو، چاہے انسان کرے یا ایجنٹ، حقیقت سے شروع کرنے دیتی ہے۔