-
Notifications
You must be signed in to change notification settings - Fork 85
feat(docs): add interactive kubernetes manifest generator for heterogeneous devices #744
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| title: Interactive Manifest Generator | ||
| sidebar_label: Manifest Generator | ||
| --- | ||
|
|
||
| # Interactive Manifest Generator | ||
|
|
||
| Project HAMi supports GPU virtualization across a variety of hardware manufacturers, including NVIDIA, Cambricon, Hygon, Iluvatar, and Huawei. Each device requires specific Kubernetes resource keys in your container `resources.limits` to correctly allocate device memory and cores, and specific `metadata.annotations` to constrain to device types or UUIDs. | ||
|
|
||
| Use the interactive tool below to generate the exact YAML configuration needed for your use case. You can integrate the generated configuration directly into your deployment specifications (such as adding the resources to your `spec.template.spec.containers` section). | ||
|
|
||
| import ManifestGenerator from '@site/src/components/ManifestGenerator'; | ||
|
|
||
| <ManifestGenerator /> | ||
|
|
||
| ## Advanced Options | ||
|
|
||
| - **Specific Device Type**: If you have a heterogeneous cluster (e.g. A100s and V100s), you can specify which device model your pod should be scheduled on. | ||
| - **Specific Device UUID**: If you need to bind a pod to a specific physical device for performance profiling or debugging, you can provide its UUID. | ||
|
|
||
| > **Note**: Not all vendors support core percentage or memory percentage scaling. The generator automatically adapts its options based on the selected device vendor's supported capabilities. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| title: 交互式 Manifest 生成器 | ||
| sidebar_label: Manifest 生成器 | ||
| --- | ||
|
|
||
| # 交互式 Manifest 生成器 | ||
|
|
||
| Project HAMi 支持跨多个硬件制造商的 GPU 虚拟化,包括 NVIDIA、寒武纪 (Cambricon)、海光 (Hygon)、天数智芯 (Iluvatar) 和华为升腾 (Huawei Ascend)。每个设备需要在您的容器 `resources.limits` 中使用特定的 Kubernetes 资源键以便正确分配设备内存和核心,并使用特定的 `metadata.annotations` 来限制设备类型或 UUID。 | ||
|
|
||
| 使用下方的交互式工具,为您的用例生成准确的 YAML 配置。您可以将生成的配置直接集成到您的部署规范中(例如将资源添加到您的 `spec.template.spec.containers` 部分)。 | ||
|
|
||
| import ManifestGenerator from '@site/src/components/ManifestGenerator'; | ||
|
|
||
| <ManifestGenerator /> | ||
|
|
||
| ## 高级选项 (Advanced Options) | ||
|
|
||
| - **特定设备类型 (Specific Device Type)**: 如果您有一个异构集群(例如 A100 和 V100 混合),您可以指定您的 Pod 应该调度到哪种设备型号上。 | ||
| - **特定设备 UUID (Specific Device UUID)**: 如果您出于性能分析或调试目的,需要将 Pod 绑定到特定的物理设备,您可以提供其 UUID。 | ||
|
|
||
| > **注意**: 并非所有供应商都支持核心百分比或内存百分比分配。生成器会根据所选设备供应商支持的功能,自动调整其选项。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| import React, { useState, useMemo } from 'react'; | ||
| import CodeBlock from '@theme/CodeBlock'; | ||
| import Translate, { translate } from '@docusaurus/Translate'; | ||
| import clsx from 'clsx'; | ||
| import styles from './styles.module.css'; | ||
|
|
||
| const VENDORS = { | ||
| NVIDIA: { | ||
| name: 'NVIDIA (GPU)', | ||
| resourceKey: 'nvidia.com/gpu', | ||
| memKey: 'nvidia.com/gpumem', | ||
| memPctKey: 'nvidia.com/gpumem-percentage', | ||
| coreKey: 'nvidia.com/gpucores', | ||
| corePctKey: 'nvidia.com/gpucores-percentage', | ||
| typeKey: 'hami.io/vgpu-type', | ||
| uuidKey: 'hami.io/vgpu-uuid', | ||
| memUnit: 'MiB', | ||
| }, | ||
| CAMBRICON: { | ||
| name: 'Cambricon (MLU)', | ||
| resourceKey: 'cambricon.com/vmlu', | ||
| memKey: 'cambricon.com/mlu.smlu.vmemory', | ||
| memPctKey: 'cambricon.com/mlu.smlu.vmemory', | ||
| coreKey: 'cambricon.com/mlu.smlu.smlu', | ||
| corePctKey: 'cambricon.com/mlu.smlu.smlu', | ||
| typeKey: 'hami.io/mlu-type', | ||
| uuidKey: 'hami.io/mlu-uuid', | ||
| memUnit: '%', | ||
| }, | ||
| HYGON: { | ||
| name: 'Hygon (DCU)', | ||
| resourceKey: 'hygon.com/dcunum', | ||
| memKey: 'hygon.com/dcumem', | ||
| memPctKey: null, | ||
| coreKey: 'hygon.com/dcucores', | ||
| corePctKey: null, | ||
| typeKey: 'hami.io/dcu-type', | ||
| uuidKey: 'hami.io/dcu-uuid', | ||
| memUnit: 'MiB', | ||
| }, | ||
| ILUVATAR: { | ||
| name: 'Iluvatar (GPU)', | ||
| resourceKey: 'iluvatar.ai/vgpu', | ||
| memKey: 'iluvatar.ai/vcuda-memory', | ||
| memPctKey: null, | ||
| coreKey: 'iluvatar.ai/vcuda-core', | ||
| corePctKey: null, | ||
| typeKey: 'hami.io/iluvatar-type', | ||
| uuidKey: 'hami.io/iluvatar-uuid', | ||
| memUnit: 'MiB', | ||
| }, | ||
| ASCEND: { | ||
| name: 'Huawei Ascend (NPU)', | ||
| resourceKey: 'huawei.com/Ascend910', | ||
| memKey: 'huawei.com/Ascend910-memory', | ||
| memPctKey: null, | ||
| coreKey: null, | ||
| corePctKey: null, | ||
| typeKey: null, | ||
| uuidKey: null, | ||
| memUnit: 'MiB', | ||
| }, | ||
| }; | ||
|
|
||
| export default function ManifestGenerator() { | ||
| const [vendor, setVendor] = useState('NVIDIA'); | ||
| const [deviceCount, setDeviceCount] = useState(1); | ||
| const [memMode, setMemMode] = useState('value'); | ||
| const [memValue, setMemValue] = useState(3000); | ||
| const [coreMode, setCoreMode] = useState('none'); | ||
| const [coreValue, setCoreValue] = useState(50); | ||
|
|
||
| const [advanced, setAdvanced] = useState(false); | ||
| const [deviceType, setDeviceType] = useState(''); | ||
| const [deviceUuid, setDeviceUuid] = useState(''); | ||
|
|
||
| // Compute yamlCode synchronously for bulletproof SSR | ||
| const yamlCode = useMemo(() => { | ||
| const v = VENDORS[vendor]; | ||
| let annotations = []; | ||
| let limits = []; | ||
|
|
||
| limits.push(` ${v.resourceKey}: ${deviceCount}`); | ||
|
|
||
| if (v.memKey) { | ||
| if (memMode === 'value' && v.memUnit !== '%') { | ||
| limits.push(` ${v.memKey}: ${memValue}`); | ||
| } else if (memMode === 'percentage' && v.memPctKey) { | ||
| limits.push(` ${v.memPctKey}: ${memValue}`); | ||
| } else if (v.memUnit === '%') { | ||
| // Fallback if forced percentage logic | ||
| limits.push(` ${v.memPctKey || v.memKey}: ${memValue}`); | ||
| } | ||
| } | ||
|
|
||
| if (v.coreKey && coreMode !== 'none') { | ||
| if (coreMode === 'value') { | ||
| limits.push(` ${v.coreKey}: ${coreValue}`); | ||
| } else if (coreMode === 'percentage' && v.corePctKey) { | ||
| limits.push(` ${v.corePctKey}: ${coreValue}`); | ||
| } | ||
| } | ||
|
|
||
| if (advanced) { | ||
| if (deviceType && v.typeKey) { | ||
| annotations.push(` ${v.typeKey}: ${JSON.stringify(deviceType)}`); | ||
| } | ||
| if (deviceUuid && v.uuidKey) { | ||
| annotations.push(` ${v.uuidKey}: ${JSON.stringify(deviceUuid)}`); | ||
| } | ||
| } | ||
|
|
||
| let code = `apiVersion: v1 | ||
| kind: Pod | ||
| metadata: | ||
| name: hami-${vendor.toLowerCase()}-pod | ||
| `; | ||
|
|
||
| if (annotations.length > 0) { | ||
| code += ` annotations:\n${annotations.join('\n')}\n`; | ||
| } | ||
|
|
||
| code += `spec: | ||
| containers: | ||
| - name: hami-container | ||
| image: ubuntu:22.04 | ||
| command: ["sleep", "infinity"] | ||
| resources: | ||
| limits: | ||
| ${limits.join('\n')}`; | ||
|
|
||
| return code; | ||
| }, [vendor, deviceCount, memMode, memValue, coreMode, coreValue, advanced, deviceType, deviceUuid]); | ||
|
|
||
| const vInfo = VENDORS[vendor]; | ||
|
|
||
| return ( | ||
| <div className={styles.generatorContainer}> | ||
| <div className={styles.controlsPanel}> | ||
| <h3> | ||
| <Translate id="manifest.generator.title">Resource Request Configuration</Translate> | ||
| </h3> | ||
|
|
||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="deviceVendor"> | ||
| <Translate id="manifest.generator.vendor">Device Vendor</Translate> | ||
| </label> | ||
| <select id="deviceVendor" value={vendor} onChange={(e) => { | ||
| setVendor(e.target.value); | ||
| setMemMode(VENDORS[e.target.value].memUnit === '%' ? 'percentage' : 'value'); | ||
| }} className={styles.select}> | ||
| {Object.entries(VENDORS).map(([key, val]) => ( | ||
| <option key={key} value={key}>{val.name}</option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
|
|
||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="deviceCount"> | ||
| <Translate id="manifest.generator.deviceCount">Number of Devices Requested</Translate> | ||
| </label> | ||
| <input id="deviceCount" type="number" min="1" value={deviceCount} onChange={e => setDeviceCount(Math.max(1, parseInt(e.target.value) || 1))} className={styles.input} /> | ||
| </div> | ||
|
|
||
| {vInfo.memKey && ( | ||
| <div className={styles.flexRow}> | ||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="memMode"> | ||
| <Translate id="manifest.generator.memMode">Memory Allocation Type</Translate> | ||
| </label> | ||
| <select id="memMode" value={memMode} onChange={e => setMemMode(e.target.value)} className={styles.select} disabled={vInfo.memUnit === '%'}> | ||
| {vInfo.memUnit !== '%' && ( | ||
| <option value="value"> | ||
| {translate({ id: 'manifest.generator.memMode.absolute', message: 'Absolute (MiB)' })} | ||
| </option> | ||
| )} | ||
| {(vInfo.memPctKey || vInfo.memUnit === '%') && ( | ||
| <option value="percentage"> | ||
| {translate({ id: 'manifest.generator.memMode.percentage', message: 'Percentage (%)' })} | ||
| </option> | ||
| )} | ||
| </select> | ||
| </div> | ||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="memValue"> | ||
| <Translate id="manifest.generator.memValue">Memory Value</Translate> | ||
| </label> | ||
| <input id="memValue" type="number" min="0" value={memValue} onChange={e => setMemValue(Math.max(0, parseInt(e.target.value) || 0))} className={styles.input} /> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {vInfo.coreKey && ( | ||
| <div className={styles.flexRow}> | ||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="coreMode"> | ||
| <Translate id="manifest.generator.coreMode">Core Allocation</Translate> | ||
| </label> | ||
| <select id="coreMode" value={coreMode} onChange={e => setCoreMode(e.target.value)} className={styles.select}> | ||
| <option value="none"> | ||
| {translate({ id: 'manifest.generator.coreMode.none', message: 'None (Default)' })} | ||
| </option> | ||
| <option value="value"> | ||
| {translate({ id: 'manifest.generator.coreMode.absolute', message: 'Absolute Cores' })} | ||
| </option> | ||
| {vInfo.corePctKey && ( | ||
| <option value="percentage"> | ||
| {translate({ id: 'manifest.generator.coreMode.percentage', message: 'Percentage (%)' })} | ||
| </option> | ||
| )} | ||
| </select> | ||
| </div> | ||
| {coreMode !== 'none' && ( | ||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="coreValue"> | ||
| <Translate id="manifest.generator.coreValue">Core Value</Translate> | ||
| </label> | ||
| <input id="coreValue" type="number" min="0" value={coreValue} onChange={e => setCoreValue(Math.max(0, parseInt(e.target.value) || 0))} className={styles.input} /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| )} | ||
|
|
||
| <button type="button" aria-expanded={advanced} className={clsx(styles.advancedToggle, styles.interactiveText)} onClick={() => setAdvanced(!advanced)} style={{ background: 'none', border: 'none', padding: 0 }}> | ||
| <span> | ||
| {advanced ? '▼ ' : '▶ '} | ||
| <Translate id="manifest.generator.advanced">Advanced Configurations (Device Type / UUID)</Translate> | ||
| </span> | ||
| </button> | ||
|
|
||
| {advanced && vInfo.typeKey && ( | ||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="deviceType"> | ||
| <Translate id="manifest.generator.deviceType">Specific Device Type Constraints (e.g. NVIDIA-A100)</Translate> | ||
| </label> | ||
| <input id="deviceType" type="text" value={deviceType} onChange={e => setDeviceType(e.target.value)} placeholder={translate({ id: 'manifest.generator.emptyPlaceholder', message: 'Leave empty for any' })} className={styles.input} /> | ||
| </div> | ||
| )} | ||
|
|
||
| {advanced && vInfo.uuidKey && ( | ||
| <div className={styles.inputGroup}> | ||
| <label htmlFor="deviceUuid"> | ||
| <Translate id="manifest.generator.deviceUuid">Specific Device UUID (e.g. GPU-fef808...)</Translate> | ||
| </label> | ||
| <input id="deviceUuid" type="text" value={deviceUuid} onChange={e => setDeviceUuid(e.target.value)} placeholder={translate({ id: 'manifest.generator.emptyPlaceholder', message: 'Leave empty for any' })} className={styles.input} /> | ||
| </div> | ||
| )} | ||
|
|
||
| </div> | ||
|
|
||
| <div className={styles.previewPanel}> | ||
| <h3> | ||
| <Translate id="manifest.generator.previewTitle">Generated YAML Manifest</Translate> | ||
| </h3> | ||
| <p> | ||
| <Translate id="manifest.generator.previewDesc">Integrate this into your Kubernetes Pod or Deployment spec.</Translate> | ||
| </p> | ||
|
Comment on lines
+255
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not describe a Pod manifest as directly usable in a Deployment spec. The generated YAML declares
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| <CodeBlock language="yaml" title="pod.yaml"> | ||
| {yamlCode} | ||
| </CodeBlock> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reset an unsupported core allocation mode after a vendor change.
If a user selects NVIDIA percentage cores and then selects Hygon or Iluvatar,
coreModeremainspercentage. The UI shows a core value, but the generator emits no core limit because the new vendor has nocorePctKey. ResetcoreModetononewhen the next vendor does not support the selected mode.🤖 Prompt for AI Agents