app.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. // Container Image Selector - Main Logic
  2. const CONFIG = {
  3. versionsUrl: './versions/index.json',
  4. versionsBaseUrl: './versions/',
  5. registries: {
  6. 'docker-hub': {
  7. name: 'Docker Hub', prefix: 'gpustack/', registry: 'docker.io',
  8. // Special handling for Docker Hub: point to official image paths
  9. overrides: { 'postgres': 'postgres', 'prometheus': 'prom/prometheus', 'grafana': 'grafana/grafana' }
  10. },
  11. 'quay': { name: 'Quay.io', prefix: 'quay.io/gpustack/', registry: 'quay.io' },
  12. 'china': { name: '国内镜像源', prefix: 'swr.cn-south-1.myhuaweicloud.com/gpustack/', registry: 'swr.cn-south-1.myhuaweicloud.com' }
  13. },
  14. // Mapping of card types to acceleration frameworks
  15. cardFrameworkMap: {
  16. 'nvidia': 'CUDA', 'amd': 'ROCm', 'ascend': 'CANN', 'hygon': 'DTK',
  17. 'mthreads': 'MUSA', 'iluvatar': 'CoreX', 'cambricon': 'Neuware',
  18. 'maca': 'MACA', 't-head': 'HGGC'
  19. },
  20. // Standard case mapping for inference engine names
  21. backendNameMap: {
  22. 'vllm': 'vLLM', 'sglang': 'SGLang', 'mindie': 'MindIE', 'voxbox': 'VoxBox'
  23. },
  24. // Multilingual dictionary
  25. i18n: {
  26. 'zh': {
  27. 'title': '容器镜像选择器',
  28. 'subtitle': '容器镜像选择器',
  29. 'back_to_docs': '文档',
  30. 'version': '版本',
  31. 'loading': '加载中...',
  32. 'select_config': '选择配置',
  33. 'gpu_type': 'GPU 类型',
  34. 'framework_version': '计算框架版本',
  35. 'select_gpu_first': '请选择 GPU 类型',
  36. 'inference_backend': '推理后端',
  37. 'inference_backend_tooltip': '如果未找到所需的内置推理后端或对应版本,可尝试切换到较低版本的计算框架。一般来说,高版本驱动能够兼容运行低版本的计算框架。',
  38. 'optional_images': '可选镜像',
  39. 'optional_postgres_images_tooltip': '使用外部数据库:<a href="https://docs.gpustack.ai/latest/installation/installation/#using-an-external-database" target="_blank">https://docs.gpustack.ai/latest/installation/installation/#using-an-external-database</a>',
  40. 'optional_monitoring_images_tooltip': '配置外部可观测性:<a href="https://docs.gpustack.ai/latest/user-guide/observability/#external-observability-optional" target="_blank">https://docs.gpustack.ai/latest/user-guide/observability/#external-observability-optional</a>',
  41. 'postgres': 'PostgreSQL',
  42. 'monitoring': '监控套件 (Prometheus + Grafana)',
  43. 'required_images': '所需镜像',
  44. 'architecture': '架构',
  45. 'registry': '镜像源',
  46. 'china_registry': '国内镜像源',
  47. 'tab_all': '全部',
  48. 'tab_server': 'Server 节点',
  49. 'tab_worker': 'Worker 节点',
  50. 'placeholder_select': '请在左侧选择配置以生成镜像列表',
  51. 'no_images': '# 未找到匹配的镜像',
  52. 'offline_guide_title': '需要离线安装?',
  53. 'guide_tab_save': '镜像文件导入 (Save & Load)',
  54. 'guide_tab_tag': '推送至私有仓库 (Tag & Push)',
  55. 'guide_tab_auto': '自动化镜像同步',
  56. 'guide_save_content': `
  57. <p>适用于完全物理隔离的环境,通过镜像文件离线导入。</p>
  58. <ol>
  59. <li>在联网机器拉取镜像(参考上方镜像拉取命令)。</li>
  60. <li>导出镜像为打包文件:
  61. <pre><code>docker save -o gpustack-server-images.tar {{server_images}}\n\ndocker save -o gpustack-worker-images.tar {{worker_images}}</code></pre>
  62. </li>
  63. <li>将镜像文件拷贝至离线机器。</li>
  64. <li>按节点角色导入镜像:
  65. <div style="margin-top:10px"><strong>Server 节点:</strong></div>
  66. <pre><code>docker load -i gpustack-server-images.tar</code></pre>
  67. <div style="margin-top:10px"><strong>Worker 节点:</strong></div>
  68. <pre><code>docker load -i gpustack-worker-images.tar</code></pre>
  69. <div style="margin-top:10px"><strong>Server + Worker 节点:</strong></div>
  70. <pre><code>docker load -i gpustack-server-images.tar\ndocker load -i gpustack-worker-images.tar</code></pre>
  71. </li>
  72. <li>
  73. 运行 GPUStack Server 和 Worker 容器时通过 <code class="inline-code">--system-default-container-registry</code> 参数指定镜像源:
  74. <pre><code>sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n {{registry}}/gpustack/gpustack:{{version}} \\\n --system-default-container-registry {{registry}}</code></pre>
  75. </li>
  76. </ol>`,
  77. 'guide_tag_content': `
  78. <p>适用于内网已有私有仓库(如 Harbor, Nexus)的场景。</p>
  79. <ol>
  80. <li>在联网机器拉取镜像(参考上方镜像拉取命令)。</li>
  81. <li>重新打标签并推送至私有仓库:
  82. <pre><code>export PrivateRegistry=&lt;您的私有仓库地址&gt;\n{{tag_push_commands}}</code></pre>
  83. </li>
  84. <li>运行 GPUStack Server 和 Worker 容器时,通过启动参数指定镜像源:
  85. <pre><code>sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n $PrivateRegistry/gpustack/gpustack:{{version}} \\\n --system-default-container-registry $PrivateRegistry</code></pre>
  86. </li>
  87. </ol>`,
  88. 'guide_auto_content': `
  89. <p>若需要更自动化的镜像同步手段,GPUStack 提供镜像管理命令,用于同步与管理所需镜像:</p>
  90. <ul style="list-style:none; padding-left:0">
  91. <li style="margin-bottom:8px"><code>gpustack copy-images</code>:从源仓库同步镜像到目标仓库</li>
  92. <li style="margin-bottom:8px"><code>gpustack save-images</code>:下载并保存镜像到本地路径</li>
  93. <li style="margin-bottom:8px"><code>gpustack load-images</code>:导入本地镜像包</li>
  94. <li style="margin-bottom:8px"><code>gpustack list-images</code>:列出当前版本镜像清单</li>
  95. </ul>
  96. <div style="margin-top:15px; font-size:13px; color:var(--text-color)">
  97. 以上命令均支持镜像过滤与自定义配置,具体用法请参考:
  98. <a href="https://docs.gpustack.ai/latest/installation/air-gapped/#container-images" target="_blank" style="color:var(--primary-color); font-weight:600">准备容器镜像 &rarr;</a>
  99. </div>`,
  100. 'view_full_docs': '查看完整离线部署文档 &rarr;',
  101. 'copied': '已复制到剪贴板',
  102. 'no_images': '# 未找到匹配的镜像',
  103. 'comments': {
  104. 'main': 'GPUStack 镜像 - GPUStack 核心服务,Server 和 Worker 节点均需此镜像',
  105. 'runner': '推理后端镜像',
  106. 'pause': 'Pause 镜像 - 提供模型实例容器的共享网络和 IPC 环境,仅 Docker 环境需要',
  107. 'benchmark': 'Benchmark 镜像 - 用于运行模型性能基准测试',
  108. 'postgres': 'PostgreSQL - 用于独立部署外置数据库(可选组件)',
  109. 'monitoring': '监控套件 - 包含 Prometheus 和 Grafana(可选组件)'
  110. },
  111. 'cards': {
  112. 'nvidia': 'NVIDIA', 'amd': 'AMD', 'ascend': '昇腾', 'hygon': '海光',
  113. 'mthreads': '摩尔线程', 'iluvatar': '天数智芯', 'cambricon': '寒武纪',
  114. 'maca': '沐曦', 't-head': '平头哥 PPU'
  115. }
  116. },
  117. 'en': {
  118. 'title': 'Container Image Selector',
  119. 'subtitle': 'Container Image Selector',
  120. 'back_to_docs': 'Docs',
  121. 'version': 'Version',
  122. 'loading': 'Loading...',
  123. 'select_config': 'Configuration',
  124. 'gpu_type': 'GPU Type',
  125. 'framework_version': 'Framework Version',
  126. 'select_gpu_first': 'Please select GPU Type first',
  127. 'inference_backend': 'Inference Backend',
  128. 'inference_backend_tooltip': 'If you cannot find the desired built-in inference backend or version, try switching the computing framework version to select a lower version image. High-version drivers are generally compatible with lower-version computing frameworks.',
  129. 'optional_images': 'Optional Images',
  130. 'optional_postgres_images_tooltip': 'Using an external database: <a href="https://docs.gpustack.ai/latest/installation/installation/#using-an-external-database" target="_blank">https://docs.gpustack.ai/latest/installation/installation/#using-an-external-database</a>',
  131. 'optional_monitoring_images_tooltip': 'Configuring external observability: <a href="https://docs.gpustack.ai/latest/user-guide/observability/#external-observability-optional" target="_blank">https://docs.gpustack.ai/latest/user-guide/observability/#external-observability-optional</a>',
  132. 'postgres': 'PostgreSQL',
  133. 'monitoring': 'Monitoring (Prometheus + Grafana)',
  134. 'required_images': 'Required Images',
  135. 'architecture': 'Architecture',
  136. 'registry': 'Registry',
  137. 'china_registry': 'China Mirror',
  138. 'tab_all': 'All',
  139. 'tab_server': 'Server Node',
  140. 'tab_worker': 'Worker Node',
  141. 'placeholder_select': 'Please select configuration on the left to generate image list',
  142. 'offline_guide_title': 'Need offline installation?',
  143. 'guide_tab_save': 'Image File Import (Save & Load)',
  144. 'guide_tab_tag': 'Push to Private Registry (Tag & Push)',
  145. 'guide_tab_auto': 'Automated Image Sync',
  146. 'guide_save_content': `
  147. <p>Suitable for completely air-gapped environments, importing images via files.</p>
  148. <ol>
  149. <li>Pull images on a machine with internet access (refer to commands above).</li>
  150. <li>Export images to tar files:
  151. <pre><code>docker save -o gpustack-server-images.tar {{server_images}}\n\ndocker save -o gpustack-worker-images.tar {{worker_images}}</code></pre>
  152. </li>
  153. <li>Copy files to the offline machine.</li>
  154. <li>Import images by node role:
  155. <div style="margin-top:10px"><strong>Server Node:</strong></div>
  156. <pre><code>docker load -i gpustack-server-images.tar</code></pre>
  157. <div style="margin-top:10px"><strong>Worker Node:</strong></div>
  158. <pre><code>docker load -i gpustack-worker-images.tar</code></pre>
  159. <div style="margin-top:10px"><strong>Server + Worker Node:</strong></div>
  160. <pre><code>docker load -i gpustack-server-images.tar\ndocker load -i gpustack-worker-images.tar</code></pre>
  161. </li>
  162. <li>
  163. When running GPUStack Server and Worker containers, specify the container registry using the <code class="inline-code">--system-default-container-registry</code> parameter:
  164. <pre><code>sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n {{registry}}/gpustack/gpustack:{{version}} \\\n --system-default-container-registry {{registry}}</code></pre>
  165. </li>
  166. </ol>`,
  167. 'guide_tag_content': `
  168. <p>Suitable for scenarios where a private registry (e.g., Harbor, Nexus) exists.</p>
  169. <ol>
  170. <li>Pull images on a machine with internet access (refer to commands above).</li>
  171. <li>Retag and push to the private registry:
  172. <pre><code>export PrivateRegistry=&lt;your-private-registry&gt;\n{{tag_push_commands}}</code></pre>
  173. </li>
  174. <li>Specify the image registry via start parameters when running containers:
  175. <pre><code>sudo docker run -d --name gpustack \\\n --restart unless-stopped \\\n -p 80:80 \\\n -p 10161:10161 \\\n --volume gpustack-data:/var/lib/gpustack \\\n $PrivateRegistry/gpustack/gpustack:{{version}} \\\n --system-default-container-registry $PrivateRegistry</code></pre>
  176. </li>
  177. </ol>`,
  178. 'guide_auto_content': `
  179. <p>For more automated sync methods, GPUStack provides image management commands:</p>
  180. <ul style="list-style:none; padding-left:0">
  181. <li style="margin-bottom:8px"><code>gpustack copy-images</code>: Sync images from source to destination registry</li>
  182. <li style="margin-bottom:8px"><code>gpustack save-images</code>: Download and save images to local path</li>
  183. <li style="margin-bottom:8px"><code>gpustack load-images</code>: Import images from local packages</li>
  184. <li style="margin-bottom:8px"><code>gpustack list-images</code>: List image manifest for current version</li>
  185. </ul>
  186. <div style="margin-top:15px; font-size:13px; color:var(--text-color)">
  187. All commands support filtering and custom config. For details, refer to:
  188. <a href="https://docs.gpustack.ai/latest/installation/air-gapped/#container-images" target="_blank" style="color:var(--primary-color); font-weight:600">Prepare Container Images &rarr;</a>
  189. </div>`,
  190. 'view_full_docs': 'View full air-gapped docs &rarr;',
  191. 'copied': 'Copied to clipboard',
  192. 'no_images': '# No matching images found',
  193. 'comments': {
  194. 'main': 'GPUStack Image - GPUStack core service, required for both Server and Worker nodes',
  195. 'runner': 'Inference Backend Images',
  196. 'pause': 'Pause Image - Provides shared network and IPC environment for model instance containers, required for Docker environment only',
  197. 'benchmark': 'Benchmark Image - Used for running model performance benchmarks',
  198. 'postgres': 'PostgreSQL - Used for independent deployment of external database (optional component)',
  199. 'monitoring': 'Monitoring Suite - Includes Prometheus and Grafana (optional components)'
  200. },
  201. 'cards': {
  202. 'nvidia': 'NVIDIA', 'amd': 'AMD', 'ascend': 'Ascend', 'hygon': 'Hygon',
  203. 'mthreads': 'MThreads', 'iluvatar': 'Iluvatar', 'cambricon': 'Cambricon',
  204. 'maca': 'MetaX', 't-head': 'T-Head PPU'
  205. }
  206. }
  207. }
  208. };
  209. // Global State - Default to English
  210. let state = {
  211. currentLang: localStorage.getItem('lang') || 'en',
  212. images: [], runnerImages: [], supportMatrix: {},
  213. selectedComponent: 'all', selectedArch: 'amd64', selectedRegistry: 'docker-hub',
  214. selectedCard: null, selectedFrameworkVersion: null, selectedChipType: null,
  215. selectedBackends: [], availableVersions: [], selectedGpuStackVersion: null,
  216. optionalImages: { 'postgres': false, 'monitoring': false }
  217. };
  218. // DOM Elements
  219. const elements = {};
  220. // Initialization
  221. async function init() {
  222. initElements();
  223. bindEvents();
  224. updateLanguage();
  225. await loadData();
  226. // Show content after initialization
  227. document.body.classList.add('i18n-ready');
  228. }
  229. // Initialize DOM elements
  230. function initElements() {
  231. elements.archSelector = document.getElementById('arch-selector');
  232. elements.registrySelector = document.getElementById('registry-selector');
  233. elements.gpustackVersionSelect = document.getElementById('gpustack-version-select');
  234. elements.cardSelector = document.getElementById('card-selector');
  235. elements.frameworkVersionSelect = document.getElementById('framework-version-select');
  236. elements.backendSelector = document.getElementById('backend-selector');
  237. elements.optionalImages = {
  238. 'postgres': document.getElementById('postgres'),
  239. 'monitoring': document.getElementById('monitoring')
  240. };
  241. elements.imageTabs = document.querySelectorAll('.image-tab');
  242. elements.imageList = document.getElementById('image-list');
  243. elements.copyAllBtn = document.getElementById('copy-all-btn');
  244. elements.currentLangBtn = document.getElementById('current-lang');
  245. elements.dropdownLinks = document.querySelectorAll('.dropdown-content a');
  246. elements.registryChina = document.getElementById('registry-china');
  247. elements.guideTabs = document.querySelectorAll('.guide-tab');
  248. }
  249. // Bind event listeners
  250. function bindEvents() {
  251. elements.archSelector.querySelectorAll('.option-button').forEach(btn => {
  252. btn.addEventListener('click', () => selectArch(btn.dataset.value));
  253. });
  254. elements.registrySelector.querySelectorAll('.option-button').forEach(btn => {
  255. btn.addEventListener('click', () => selectRegistry(btn.dataset.value));
  256. });
  257. elements.cardSelector.addEventListener('click', (e) => {
  258. const btn = e.target.closest('.option-button');
  259. if (btn) selectCard(btn.dataset.value);
  260. });
  261. elements.frameworkVersionSelect.addEventListener('change', (e) => selectFrameworkVersion(e.target.value));
  262. elements.backendSelector.addEventListener('change', () => updateSelectedBackends());
  263. Object.keys(elements.optionalImages).forEach(key => {
  264. elements.optionalImages[key].addEventListener('change', () => {
  265. state.optionalImages[key] = elements.optionalImages[key].checked;
  266. generateImageList();
  267. });
  268. });
  269. elements.gpustackVersionSelect.addEventListener('change', (e) => selectGpuStackVersion(e.target.value));
  270. elements.imageTabs.forEach(tab => {
  271. tab.addEventListener('click', () => selectComponent(tab.dataset.component));
  272. });
  273. elements.guideTabs.forEach(tab => {
  274. tab.addEventListener('click', () => switchGuideTab(tab.dataset.guide));
  275. });
  276. elements.dropdownLinks.forEach(link => {
  277. link.addEventListener('click', (e) => {
  278. e.preventDefault();
  279. setLanguage(link.dataset.lang);
  280. });
  281. });
  282. }
  283. // Set application language
  284. function setLanguage(lang) {
  285. if (state.currentLang === lang) return;
  286. state.currentLang = lang;
  287. localStorage.setItem('lang', lang);
  288. updateLanguage();
  289. // Auto-switch to Docker Hub if China Mirror is selected but language is English
  290. if (state.currentLang === 'en' && state.selectedRegistry === 'china') {
  291. selectRegistry('docker-hub');
  292. }
  293. renderCardSelector();
  294. generateImageList();
  295. }
  296. // Get the current set of selected images
  297. function getCurrentImages(component) {
  298. const imgs = [];
  299. if (state.selectedGpuStackVersion) imgs.push(getFullImageName('gpustack', state.selectedGpuStackVersion, state.selectedRegistry));
  300. if (!state.selectedCard || !state.selectedFrameworkVersion) return imgs;
  301. const fwTag = CONFIG.cardFrameworkMap[state.selectedCard].toLowerCase() + state.selectedFrameworkVersion;
  302. if (component === 'worker' || component === 'all') {
  303. state.runnerImages.forEach(img => {
  304. const tag = img.replace('gpustack/runner:', '');
  305. if (!tag.startsWith(fwTag)) return;
  306. if (state.selectedCard === 'ascend' && state.selectedChipType) {
  307. const pts = tag.split('-');
  308. if (pts[1] && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b)) && pts[1] !== state.selectedChipType) return;
  309. }
  310. if (state.selectedBackends.length > 0) {
  311. const pts = tag.split('-');
  312. let bPart = (pts.length >= 3 && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b))) ? pts[2] : pts[1];
  313. const m = bPart?.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
  314. if (!m || !state.selectedBackends.includes(`${m[1].toLowerCase()}-${m[2]}`)) return;
  315. }
  316. imgs.push(getFullImageName('runner', tag, state.selectedRegistry));
  317. });
  318. const pause = state.images.find(i => i.includes('runtime:pause'));
  319. if (pause) imgs.push(getFullImageName('runtime', pause.split(':')[1], state.selectedRegistry));
  320. const bm = state.images.find(i => i.includes('benchmark-runner'));
  321. if (bm) imgs.push(getFullImageName('benchmark-runner', bm.split(':')[1], state.selectedRegistry));
  322. }
  323. if (component === 'server' || component === 'all') {
  324. if (state.optionalImages['postgres']) {
  325. const pgs = state.images.filter(i => i.startsWith('postgres:'));
  326. pgs.forEach(i => imgs.push(getFullImageName('postgres', i.split(':')[1], state.selectedRegistry)));
  327. }
  328. if (state.optionalImages['monitoring']) {
  329. state.images.filter(i => i.includes('prometheus')).forEach(i => imgs.push(getFullImageName('prometheus', i.split(':')[1], state.selectedRegistry)));
  330. state.images.filter(i => i.includes('grafana')).forEach(i => imgs.push(getFullImageName('grafana', i.split(':')[1], state.selectedRegistry)));
  331. }
  332. }
  333. return imgs;
  334. }
  335. // Update UI text based on current language
  336. function updateLanguage() {
  337. const lang = state.currentLang;
  338. const data = CONFIG.i18n[lang];
  339. const version = state.selectedGpuStackVersion || 'latest';
  340. // Translate page title
  341. document.title = data.title;
  342. elements.currentLangBtn.textContent = lang === 'zh' ? '简体中文' : 'English';
  343. // Get current image lists for dynamic placeholders
  344. const allImgs = getCurrentImages('all');
  345. const serverImgs = getCurrentImages('server').join(' ');
  346. const workerImgs = getCurrentImages('worker').join(' ');
  347. // Generate tag & push commands under 'gpustack' namespace
  348. const tagPushCmds = allImgs.map(img => {
  349. const parts = img.split(':');
  350. const namePart = parts[0];
  351. const tagPart = parts[1];
  352. const shortName = namePart.includes('/') ? namePart.split('/').pop() : namePart;
  353. const destImg = `gpustack/${shortName}:${tagPart}`;
  354. return `docker tag ${img} $PrivateRegistry/${destImg}\ndocker push $PrivateRegistry/${destImg}`;
  355. }).join('\n');
  356. document.querySelectorAll('[data-i18n]').forEach(el => {
  357. const key = el.dataset.i18n;
  358. if (data[key]) {
  359. let content = data[key];
  360. // Dynamically replace placeholders
  361. content = content.replace(/{{version}}/g, version);
  362. content = content.replace(/{{server_images}}/g, serverImgs || '&lt;Server Image List&gt;');
  363. content = content.replace(/{{worker_images}}/g, workerImgs || '&lt;Worker Image List&gt;');
  364. content = content.replace(/{{tag_push_commands}}/g, tagPushCmds || '# Please select configuration to generate commands');
  365. content = content.replace(/{{registry}}/g, CONFIG.registries[state.selectedRegistry].registry);
  366. el.innerHTML = content;
  367. }
  368. });
  369. const copyImagesEl = document.getElementById('copy-images-command');
  370. if (copyImagesEl) copyImagesEl.textContent = data.copy_images_cmd;
  371. // Hide China Mirror in English mode
  372. elements.registryChina.style.display = (lang === 'en') ? 'none' : 'block';
  373. }
  374. // Load versions and initial data
  375. async function loadData() {
  376. try {
  377. const response = await fetch(CONFIG.versionsUrl);
  378. const data = await response.json();
  379. state.availableVersions = data.versions || data;
  380. state.selectedGpuStackVersion = state.availableVersions[0];
  381. renderVersionSelector();
  382. await loadVersionData(state.selectedGpuStackVersion);
  383. } catch (error) {
  384. console.error('Failed to load data:', error);
  385. showToast(state.currentLang === 'zh' ? '加载数据失败' : 'Failed to load data');
  386. }
  387. }
  388. // Load specific version data
  389. async function loadVersionData(version) {
  390. const response = await fetch(`${CONFIG.versionsBaseUrl}${version}.json`);
  391. const data = await response.json();
  392. state.images = data;
  393. state.runnerImages = data.filter(img => img.startsWith('gpustack/runner:'));
  394. parseSupportMatrix();
  395. renderCardSelector();
  396. updateLanguage(); // Refresh offline guide version number
  397. }
  398. // Render version dropdown
  399. function renderVersionSelector() {
  400. elements.gpustackVersionSelect.innerHTML = '';
  401. state.availableVersions.forEach(v => {
  402. const opt = document.createElement('option');
  403. opt.value = opt.textContent = v;
  404. opt.selected = (v === state.selectedGpuStackVersion);
  405. elements.gpustackVersionSelect.appendChild(opt);
  406. });
  407. }
  408. // Handle version change
  409. async function selectGpuStackVersion(v) {
  410. state.selectedGpuStackVersion = v;
  411. await loadVersionData(v);
  412. generateImageList();
  413. }
  414. // Parse supported hardware and backends from images
  415. function parseSupportMatrix() {
  416. const matrix = { frameworks: {} };
  417. state.runnerImages.forEach(image => {
  418. const tag = image.replace('gpustack/runner:', '');
  419. let parts = tag.split('-');
  420. const fwMatch = parts[0].match(/^([a-z]+)([\d.]+)$/i);
  421. if (!fwMatch) return;
  422. const fw = fwMatch[1].toLowerCase();
  423. if (!matrix.frameworks[fw]) matrix.frameworks[fw] = { versions: new Set(), cards: new Set(), backends: new Set() };
  424. matrix.frameworks[fw].versions.add(fwMatch[2]);
  425. let hasCard = false;
  426. if (parts.length >= 2 && /^[a-z0-9]+$/i.test(parts[1]) && !/^\d/.test(parts[1]) && !['vllm','sglang','mindie','voxbox'].some(b => parts[1].includes(b))) {
  427. matrix.frameworks[fw].cards.add(parts[1]);
  428. hasCard = true;
  429. }
  430. const backendPart = hasCard ? parts[2] : parts[1];
  431. if (backendPart) {
  432. const bMatch = backendPart.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
  433. if (bMatch) matrix.frameworks[fw].backends.add(bMatch[1].toLowerCase());
  434. }
  435. });
  436. state.supportMatrix = { frameworks: {} };
  437. Object.keys(matrix.frameworks).forEach(fw => {
  438. state.supportMatrix.frameworks[fw] = {
  439. versions: Array.from(matrix.frameworks[fw].versions),
  440. cards: Array.from(matrix.frameworks[fw].cards),
  441. backends: Array.from(matrix.frameworks[fw].backends)
  442. };
  443. });
  444. }
  445. // Render GPU Type buttons
  446. function renderCardSelector() {
  447. elements.cardSelector.innerHTML = '';
  448. const cardDict = CONFIG.i18n[state.currentLang].cards;
  449. const allCards = [
  450. { id: 'nvidia', name: cardDict.nvidia, framework: 'CUDA' },
  451. { id: 'amd', name: cardDict.amd, framework: 'ROCm' },
  452. { id: 'ascend', name: cardDict.ascend, framework: 'CANN' },
  453. { id: 'hygon', name: cardDict.hygon, framework: 'DTK' },
  454. { id: 'mthreads', name: cardDict.mthreads, framework: 'MUSA' },
  455. { id: 'iluvatar', name: cardDict.iluvatar, framework: 'CoreX' },
  456. { id: 'cambricon', name: cardDict.cambricon, framework: 'Neuware' },
  457. { id: 'maca', name: cardDict.maca, framework: 'MACA' },
  458. { id: 't-head', name: cardDict.t_head || cardDict['t-head'], framework: 'HGGC' }
  459. ];
  460. allCards.forEach(card => {
  461. const framework = card.framework.toLowerCase();
  462. const hasData = state.supportMatrix.frameworks[framework];
  463. const btn = document.createElement('button');
  464. btn.className = 'option-button';
  465. btn.dataset.value = card.id;
  466. btn.innerHTML = `<span>${card.name}</span>`;
  467. if (card.id === 'cambricon') {
  468. const tip = state.currentLang === 'zh' ? '请联系寒武纪厂商获取推理后端镜像' : 'Please contact Cambricon vendor for inference backend images';
  469. btn.innerHTML += `<span class="tooltip-icon">i <span class="tooltip-content">${tip}</span></span>`;
  470. }
  471. btn.disabled = !hasData;
  472. if (!hasData) btn.title = state.currentLang === 'zh' ? '暂无可用镜像' : 'No images available';
  473. elements.cardSelector.appendChild(btn);
  474. });
  475. const activeBtn = elements.cardSelector.querySelector(`[data-value="${state.selectedCard}"]`) || elements.cardSelector.querySelector('[data-value="nvidia"]');
  476. if (activeBtn && !activeBtn.disabled) selectCard(activeBtn.dataset.value);
  477. }
  478. // Handle GPU Type selection
  479. function selectCard(cardId) {
  480. state.selectedCard = cardId;
  481. elements.cardSelector.querySelectorAll('.option-button').forEach(b => b.classList.toggle('active', b.dataset.value === cardId));
  482. const fw = CONFIG.cardFrameworkMap[cardId].toLowerCase();
  483. renderFrameworkVersions(fw, cardId);
  484. const first = elements.frameworkVersionSelect.querySelector('option:not([value=""])');
  485. if (first) { first.selected = true; selectFrameworkVersion(first.value); }
  486. const targetArch = cardId === 'ascend' ? 'arm64' : 'amd64';
  487. if (state.selectedArch !== targetArch) selectArch(targetArch);
  488. }
  489. // Render Framework Version dropdown
  490. function renderFrameworkVersions(fw, cardId) {
  491. const select = elements.frameworkVersionSelect;
  492. const placeholder = state.currentLang === 'zh' ? '请选择版本' : 'Please select version';
  493. select.innerHTML = `<option value="">${placeholder}</option>`;
  494. const versions = state.supportMatrix.frameworks[fw]?.versions || [];
  495. const fwName = CONFIG.cardFrameworkMap[cardId];
  496. if (cardId === 'ascend') {
  497. const combos = new Map();
  498. state.runnerImages.forEach(img => {
  499. const tag = img.replace('gpustack/runner:', '');
  500. if (!tag.startsWith(fw) || !tag.startsWith('cann')) return;
  501. const pts = tag.split('-');
  502. if (pts.length < 2) return;
  503. const vMatch = pts[0].match(/^cann([\d.]+)$/i);
  504. if (vMatch && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b))) combos.set(`${vMatch[1]}-${pts[1]}`, { v: vMatch[1], c: pts[1] });
  505. });
  506. Array.from(combos.values()).sort((a,b) => {
  507. const versionCompare = b.v.localeCompare(a.v, undefined, {numeric: true, sensitivity: 'base'});
  508. if (versionCompare !== 0) {
  509. return versionCompare;
  510. }
  511. const aIs910B = a.c.toUpperCase().includes('910B');
  512. const bIs910B = b.c.toUpperCase().includes('910B');
  513. if (aIs910B && !bIs910B) return -1;
  514. if (!aIs910B && bIs910B) return 1;
  515. return b.c.localeCompare(a.c);
  516. }).forEach(item => {
  517. const opt = document.createElement('option');
  518. opt.value = item.v; opt.dataset.chipType = item.c; opt.textContent = `${fwName} ${item.v} (${item.c})`;
  519. select.appendChild(opt);
  520. });
  521. } else {
  522. versions.sort((a,b) => b.localeCompare(a)).forEach(v => {
  523. const opt = document.createElement('option');
  524. opt.value = v; opt.textContent = `${fwName} ${v}`;
  525. select.appendChild(opt);
  526. });
  527. }
  528. }
  529. // Handle Framework Version selection
  530. function selectFrameworkVersion(v) {
  531. state.selectedFrameworkVersion = v;
  532. const opt = elements.frameworkVersionSelect.selectedOptions[0];
  533. state.selectedChipType = opt?.dataset.chipType || null;
  534. const fw = CONFIG.cardFrameworkMap[state.selectedCard].toLowerCase();
  535. renderBackends(fw, v, state.selectedChipType);
  536. state.selectedBackends = [];
  537. generateImageList();
  538. }
  539. // Render Inference Backend checkboxes
  540. function renderBackends(fw, v, chip) {
  541. elements.backendSelector.innerHTML = '';
  542. if (!v) return;
  543. const fwTag = fw + v;
  544. const options = new Map();
  545. state.runnerImages.forEach(img => {
  546. const tag = img.replace('gpustack/runner:', '');
  547. if (!tag.startsWith(fwTag)) return;
  548. const pts = tag.split('-');
  549. let bPart = pts[1];
  550. let hasCard = pts.length >= 3 && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b));
  551. if (hasCard) { if (state.selectedCard === 'ascend' && pts[1] !== chip) return; bPart = pts[2]; }
  552. const m = bPart?.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
  553. if (m) options.set(`${m[1]}-${m[2]}`, `${CONFIG.backendNameMap[m[1].toLowerCase()] || m[1]} ${m[2]}`);
  554. });
  555. // Sort versions in reverse order
  556. Array.from(options.entries()).sort((a,b) => b[0].localeCompare(a[0])).forEach(([k, name]) => {
  557. const lbl = document.createElement('label'); lbl.className = 'checkbox-item';
  558. lbl.innerHTML = `<input type="checkbox" value="${k}"><span>${name}</span>`;
  559. elements.backendSelector.appendChild(lbl);
  560. });
  561. }
  562. // Update selected backends
  563. function updateSelectedBackends() {
  564. state.selectedBackends = Array.from(elements.backendSelector.querySelectorAll('input:checked')).map(i => i.value);
  565. generateImageList();
  566. }
  567. // Get full image name supporting Overrides and Registry logic
  568. function getFullImageName(baseName, tag, registryKey) {
  569. const reg = CONFIG.registries[registryKey];
  570. let path = `${reg.prefix}${baseName}`;
  571. if (reg.overrides && reg.overrides[baseName]) {
  572. path = reg.overrides[baseName];
  573. }
  574. return `${path}:${tag}`;
  575. }
  576. // Main logic to generate the list of docker pull commands
  577. function generateImageList() {
  578. const plat = `--platform linux/${state.selectedArch}`;
  579. const cmds = [];
  580. const isServer = state.selectedComponent === 'server' || state.selectedComponent === 'all';
  581. const isWorker = state.selectedComponent === 'worker' || state.selectedComponent === 'all';
  582. const t = CONFIG.i18n[state.currentLang];
  583. if (state.selectedGpuStackVersion) {
  584. cmds.push(`# ${t.comments.main}`);
  585. cmds.push(`docker pull ${plat} ${getFullImageName('gpustack', state.selectedGpuStackVersion, state.selectedRegistry)}`);
  586. }
  587. if (!state.selectedCard || !state.selectedFrameworkVersion) {
  588. elements.imageList.textContent = cmds.length ? cmds.join('\n') : t.placeholder_select;
  589. elements.copyAllBtn.style.display = cmds.length ? 'flex' : 'none';
  590. if (cmds.length) elements.copyAllBtn.onclick = () => copyToClipboard(cmds.join('\n'));
  591. updateLanguage();
  592. return;
  593. }
  594. const fwTag = CONFIG.cardFrameworkMap[state.selectedCard].toLowerCase() + state.selectedFrameworkVersion;
  595. if (isWorker) {
  596. const rCmds = [];
  597. state.runnerImages.forEach(img => {
  598. const tag = img.replace('gpustack/runner:', '');
  599. if (!tag.startsWith(fwTag)) return;
  600. if (state.selectedCard === 'ascend' && state.selectedChipType) {
  601. const pts = tag.split('-');
  602. if (pts[1] && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b)) && pts[1] !== state.selectedChipType) return;
  603. }
  604. if (state.selectedBackends.length > 0) {
  605. const pts = tag.split('-');
  606. let bPart = (pts.length >= 3 && !['vllm','sglang','mindie','voxbox'].some(b => pts[1].includes(b))) ? pts[2] : pts[1];
  607. const m = bPart?.match(/^([a-z]+)([\d.]+(?:rc\d+)?(?:post\d+)?)?$/i);
  608. if (!m || !state.selectedBackends.includes(`${m[1].toLowerCase()}-${m[2]}`)) return;
  609. }
  610. rCmds.push(`docker pull ${plat} ${getFullImageName('runner', tag, state.selectedRegistry)}`);
  611. });
  612. if (rCmds.length) {
  613. const comment = t.comments.runner;
  614. cmds.push(`# ${comment}`);
  615. cmds.push(...rCmds);
  616. }
  617. const pause = state.images.find(i => i.includes('runtime:pause'));
  618. if (pause) {
  619. const comment = t.comments.pause;
  620. cmds.push(`# ${comment}`);
  621. cmds.push(`docker pull ${plat} ${getFullImageName('runtime', pause.split(':')[1], state.selectedRegistry)}`);
  622. }
  623. const bm = state.images.find(i => i.includes('benchmark-runner'));
  624. if (bm) {
  625. const comment = t.comments.benchmark;
  626. cmds.push(`# ${comment}`);
  627. cmds.push(`docker pull ${plat} ${getFullImageName('benchmark-runner', bm.split(':')[1], state.selectedRegistry)}`);
  628. }
  629. }
  630. if (isServer) {
  631. if (state.optionalImages['postgres']) {
  632. const pgs = state.images.filter(i => i.startsWith('postgres:'));
  633. if (pgs.length) {
  634. const comment = t.comments.postgres;
  635. cmds.push(`# ${comment}`);
  636. pgs.forEach(i => cmds.push(`docker pull ${plat} ${getFullImageName('postgres', i.split(':')[1], state.selectedRegistry)}`));
  637. }
  638. }
  639. if (state.optionalImages['monitoring']) {
  640. const comment = t.comments.monitoring;
  641. cmds.push(`# ${comment}`);
  642. state.images.filter(i => i.includes('prometheus')).forEach(i => cmds.push(`docker pull ${plat} ${getFullImageName('prometheus', i.split(':')[1], state.selectedRegistry)}`));
  643. state.images.filter(i => i.includes('grafana')).forEach(i => cmds.push(`docker pull ${plat} ${getFullImageName('grafana', i.split(':')[1], state.selectedRegistry)}`));
  644. }
  645. }
  646. renderImageList(cmds);
  647. updateLanguage();
  648. }
  649. // Display the generated commands in the output area
  650. function renderImageList(cmds) {
  651. if (!cmds.length) { elements.imageList.textContent = CONFIG.i18n[state.currentLang].no_images; elements.copyAllBtn.style.display = 'none'; return; }
  652. elements.imageList.textContent = cmds.join('\n');
  653. elements.copyAllBtn.style.display = 'flex';
  654. elements.copyAllBtn.onclick = () => copyToClipboard(cmds.join('\n'));
  655. }
  656. // Interaction handlers
  657. function selectArch(v) { state.selectedArch = v; elements.archSelector.querySelectorAll('.option-button').forEach(b => b.classList.toggle('active', b.dataset.value === v)); generateImageList(); }
  658. function selectRegistry(v) { state.selectedRegistry = v; elements.registrySelector.querySelectorAll('.option-button').forEach(b => b.classList.toggle('active', b.dataset.value === v)); generateImageList(); }
  659. function selectComponent(v) { state.selectedComponent = v; elements.imageTabs.forEach(t => t.classList.toggle('active', t.dataset.component === v)); generateImageList(); }
  660. function switchGuideTab(id) { elements.guideTabs.forEach(t => t.classList.toggle('active', t.dataset.guide === id)); document.querySelectorAll('.guide-content').forEach(c => c.classList.toggle('active', c.id === `guide-${id}`)); }
  661. // Utility to copy text to clipboard
  662. async function copyToClipboard(text) {
  663. try { await navigator.clipboard.writeText(text); showToast(CONFIG.i18n[state.currentLang].copied); }
  664. catch (err) { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); showToast(CONFIG.i18n[state.currentLang].copied); }
  665. }
  666. // Show temporary toast message
  667. function showToast(msg) {
  668. const t = document.createElement('div'); t.className = 'toast'; t.textContent = msg; document.body.appendChild(t);
  669. setTimeout(() => t.remove(), 2000);
  670. }
  671. // Start app when DOM is ready
  672. document.addEventListener('DOMContentLoaded', init);