mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-13 08:06:06 +00:00
fix(ai): unify GPU detection and improve group topology (#14367)
This commit is contained in:
@@ -86,6 +86,11 @@ func buildDefaultVllmImageByVersion(version string) string {
|
||||
}
|
||||
|
||||
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
|
||||
currentRTX := vllm.IsRTXVersion(currentVersion)
|
||||
targetRTX := vllm.IsRTXVersion(targetVersion)
|
||||
if currentRTX != targetRTX {
|
||||
return false
|
||||
}
|
||||
currentGB10 := vllm.IsGB10Version(currentVersion)
|
||||
targetGB10 := vllm.IsGB10Version(targetVersion)
|
||||
if currentGB10 != targetGB10 {
|
||||
|
||||
@@ -5,8 +5,13 @@ import "strings"
|
||||
const (
|
||||
gb10VersionPrefix = "nvidia-"
|
||||
gb10VersionSuffix = "-optimized-for-ds-v4-flash"
|
||||
rtxVersionSuffix = "-optimized-for-ds-v4.1-flash"
|
||||
)
|
||||
|
||||
func IsRTXVersion(version string) bool {
|
||||
return strings.HasSuffix(strings.ToLower(strings.TrimSpace(version)), rtxVersionSuffix)
|
||||
}
|
||||
|
||||
func IsGB10Version(version string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(version))
|
||||
tag := strings.TrimPrefix(normalized, gb10VersionPrefix)
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<div class="content-container__main" v-if="slots.main">
|
||||
<el-card>
|
||||
<div v-if="hasTitleContent" class="content-container__title">
|
||||
<div v-if="hasTitleContent()" class="content-container__title">
|
||||
<slot name="title">
|
||||
<div v-if="showBack" class="flex flex-wrap gap-4 sm:justify-between">
|
||||
<back-button
|
||||
@@ -60,7 +60,7 @@
|
||||
<div v-if="slots.prompt" class="prompt">
|
||||
<slot name="prompt"></slot>
|
||||
</div>
|
||||
<div class="main-content" :class="{ 'main-content--compact': !hasMainTopGap }">
|
||||
<div class="main-content" :class="{ 'main-content--compact': !hasMainTopGap() }">
|
||||
<slot name="main"></slot>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -87,12 +87,12 @@ const showBack = computed(() => {
|
||||
const { backPath, backName, backTo, reload } = prop;
|
||||
return backPath || backName || backTo || reload;
|
||||
});
|
||||
const hasTitleContent = computed(() => {
|
||||
const hasTitleContent = () => {
|
||||
return Boolean(
|
||||
slots.title || slots.leftToolBar || slots.rightToolBar || slots.toolbar || prop.divider || showBack.value,
|
||||
);
|
||||
});
|
||||
const hasMainTopGap = computed(() => hasTitleContent.value || Boolean(slots.prompt));
|
||||
};
|
||||
const hasMainTopGap = () => hasTitleContent() || Boolean(slots.prompt);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -777,6 +777,20 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate:
|
||||
'No available GPUs were detected under NUMA {node}. A group cannot be created from this node.',
|
||||
inspectNUMA:
|
||||
'Run these read-only commands in the current node’s terminal and match PCI addresses to check GPU NUMA placement:',
|
||||
numaQueryHint:
|
||||
'numa_node contains the NUMA node ID; -1 means the system has not reported a placement. If the results differ from this page, refresh and try again.',
|
||||
relationTopology: 'Relationship topology',
|
||||
relationHelp: 'Select a GPU or group to highlight its memberships.',
|
||||
selectionHelp:
|
||||
'Select GPUs of the same device type, then continue to configure the group. Dashed lines show unsaved memberships.',
|
||||
allRelations: 'Show all relationships',
|
||||
pendingChanges: 'Unsaved',
|
||||
hardware: 'Hardware placement',
|
||||
editMembers: 'Edit members',
|
||||
usageScope:
|
||||
'Associations come from application configurations managed by 1Panel on this node; memory values are sampled for the whole GPU.',
|
||||
usageTitle: 'GPU usage',
|
||||
@@ -1121,9 +1135,12 @@ const message = {
|
||||
numaAuto: 'Automatic',
|
||||
numaManual: 'Manual',
|
||||
numaNodes: 'NUMA nodes',
|
||||
numaHint: 'Leave binding off unless you need performance tuning.',
|
||||
numaOffHelp: 'Use the selected GPUs while the system schedules CPU and memory use.',
|
||||
numaAutoHelp: 'Bind CPU and memory use to the NUMA node containing the selected GPUs.',
|
||||
numaManualHelp: 'Specify CPU and memory nodes for the application without changing GPU selection.',
|
||||
numaResolved: 'Will bind to NUMA {node}.',
|
||||
numaHelp:
|
||||
'Binds the application to selected CPU and memory nodes. Automatic matching selects nodes near the GPUs.',
|
||||
'GPU selection determines which graphics devices to use; affinity determines which CPU and memory nodes to use.',
|
||||
numaRequirements: 'The image must include numactl. Enabling binding automatically adds SYS_NICE if needed.',
|
||||
numaUnknown:
|
||||
'Could not identify the CPU and memory node associated with the GPU. Leave binding off, or select nodes manually if you know the hardware layout.',
|
||||
@@ -1280,8 +1297,11 @@ const message = {
|
||||
ascendA3: 'Official Ascend A3 version for Ascend A3 series devices.',
|
||||
ascendA5: 'Official Ascend A5 version for 950DT devices.',
|
||||
ascend310P: 'Official Ascend 310P version for Atlas 300I DUO and Atlas 200I Pro devices.',
|
||||
default: 'Official standard version with CUDA 13.0, for newer drivers and newer GPU architectures.',
|
||||
dspark: 'NVIDIA GB10 dual-device custom version, using a custom image maintained by 1Panel.',
|
||||
default:
|
||||
'Official vLLM community standard version with CUDA 13.0, for newer drivers and newer GPU architectures.',
|
||||
rtxPro5000:
|
||||
'Custom version for NVIDIA RTX Pro 5000, optimized for DeepSeek-V4.1-Flash-1017. The image is maintained by 1Panel.',
|
||||
dspark: 'Custom version for two NVIDIA GB10 devices, optimized for DeepSeek-V4-Flash. The image is maintained by 1Panel.',
|
||||
},
|
||||
vllmDeployProfile: 'Deployment model',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -780,6 +780,20 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate:
|
||||
'No se detectaron GPU disponibles en NUMA {node}. No se puede crear un grupo desde este nodo.',
|
||||
inspectNUMA:
|
||||
'Ejecute estos comandos de solo lectura en el terminal del nodo actual y compare las direcciones PCI para comprobar la ubicación NUMA de las GPU:',
|
||||
numaQueryHint:
|
||||
'numa_node indica el ID del nodo NUMA; -1 significa que el sistema no ha informado de la ubicación. Si los resultados difieren de esta página, actualice e inténtelo de nuevo.',
|
||||
relationTopology: 'Topología de relaciones',
|
||||
relationHelp: 'Seleccione una GPU o un grupo para resaltar sus relaciones de pertenencia.',
|
||||
selectionHelp:
|
||||
'Seleccione GPU del mismo tipo de dispositivo y continúe para configurar el grupo. Las líneas discontinuas indican relaciones sin guardar.',
|
||||
allRelations: 'Mostrar todas las relaciones',
|
||||
pendingChanges: 'Sin guardar',
|
||||
hardware: 'Ubicación del hardware',
|
||||
editMembers: 'Editar miembros',
|
||||
usageScope:
|
||||
'Las asociaciones proceden de las configuraciones de aplicaciones gestionadas por 1Panel en este nodo; la memoria se mide para toda la GPU.',
|
||||
usageTitle: 'Uso de GPU',
|
||||
@@ -1123,9 +1137,11 @@ const message = {
|
||||
numaAuto: 'Automática',
|
||||
numaManual: 'Manual',
|
||||
numaNodes: 'Nodos NUMA',
|
||||
numaHint: 'Deje la afinidad desactivada salvo que necesite ajustar el rendimiento.',
|
||||
numaHelp:
|
||||
'Vincula la aplicación a los nodos de CPU y memoria seleccionados. La selección automática elige nodos cercanos a las GPU.',
|
||||
numaOffHelp: 'Usa las GPU seleccionadas mientras el sistema gestiona la CPU y la memoria.',
|
||||
numaAutoHelp: 'Vincula la CPU y la memoria al nodo NUMA de las GPU seleccionadas.',
|
||||
numaManualHelp: 'Especifica los nodos de CPU y memoria sin cambiar la selección de GPU.',
|
||||
numaResolved: 'Se vinculará a NUMA {node}.',
|
||||
numaHelp: 'La selección de GPU determina qué GPU usar; la afinidad determina los nodos de CPU y memoria.',
|
||||
numaRequirements:
|
||||
'La imagen debe incluir numactl. Al activar la afinidad se añade SYS_NICE automáticamente si es necesario.',
|
||||
numaUnknown:
|
||||
@@ -1286,8 +1302,10 @@ const message = {
|
||||
ascendA5: 'Versión oficial Ascend A5 para dispositivos 950DT.',
|
||||
ascend310P: 'Versión oficial Ascend 310P para dispositivos Atlas 300I DUO y Atlas 200I Pro.',
|
||||
default:
|
||||
'Versión oficial estándar con CUDA 13.0, para controladores recientes y GPU de nuevas arquitecturas.',
|
||||
dspark: 'Versión personalizada de NVIDIA GB10 para dos dispositivos, con una imagen mantenida por 1Panel.',
|
||||
'Versión estándar oficial de la comunidad vLLM con CUDA 13.0, para controladores y arquitecturas de GPU más recientes.',
|
||||
rtxPro5000:
|
||||
'Versión personalizada para NVIDIA RTX Pro 5000, optimizada para DeepSeek-V4.1-Flash-1017. La imagen es mantenida por 1Panel.',
|
||||
dspark: 'Versión personalizada para dos dispositivos NVIDIA GB10, optimizada para DeepSeek-V4-Flash. La imagen es mantenida por 1Panel.',
|
||||
},
|
||||
vllmDeployProfile: 'Modelo de despliegue',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -772,6 +772,19 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate: 'در NUMA {node} هیچ GPU قابل استفادهای شناسایی نشد. ایجاد گروه از این گره ممکن نیست.',
|
||||
inspectNUMA:
|
||||
'این فرمانهای فقطخواندنی را در پایانه گره فعلی اجرا کنید و با تطبیق آدرسهای PCI، وابستگی GPU به NUMA را بررسی کنید:',
|
||||
numaQueryHint:
|
||||
'مقدار numa_node شناسه گره NUMA است؛ -1 یعنی سیستم وابستگی را گزارش نکرده است. اگر نتایج با صفحه متفاوت است، صفحه را تازهسازی کرده و دوباره تلاش کنید.',
|
||||
relationTopology: 'توپولوژی روابط',
|
||||
relationHelp: 'برای برجسته کردن روابط عضویت، یک GPU یا گروه را انتخاب کنید.',
|
||||
selectionHelp:
|
||||
'GPUهای همنوع را انتخاب کنید و برای پیکربندی گروه ادامه دهید. خطوط نقطهچین عضویتهای ذخیرهنشده را نشان میدهند.',
|
||||
allRelations: 'نمایش همه روابط',
|
||||
pendingChanges: 'ذخیرهنشده',
|
||||
hardware: 'جایگیری سختافزار',
|
||||
editMembers: 'ویرایش اعضا',
|
||||
usageScope:
|
||||
'ارتباطها از تنظیمات برنامههای مدیریتشده توسط 1Panel در این گره گرفته میشوند؛ حافظه برای کل GPU اندازهگیری میشود.',
|
||||
usageTitle: 'استفاده از GPU',
|
||||
@@ -1105,9 +1118,12 @@ const message = {
|
||||
numaAuto: 'خودکار',
|
||||
numaManual: 'دستی',
|
||||
numaNodes: 'گرههای NUMA',
|
||||
numaHint: 'اتصال را غیرفعال بگذارید، مگر برای تنظیم عملکرد.',
|
||||
numaOffHelp: 'از GPUهای انتخابشده استفاده میشود و سیستم CPU و حافظه را مدیریت میکند.',
|
||||
numaAutoHelp: 'CPU و حافظه به گره NUMA مربوط به GPUهای انتخابشده متصل میشوند.',
|
||||
numaManualHelp: 'گرههای CPU و حافظه برنامه را بدون تغییر انتخاب GPU تعیین کنید.',
|
||||
numaResolved: 'به NUMA {node} متصل خواهد شد.',
|
||||
numaHelp:
|
||||
'برنامه را به گرههای پردازنده و حافظه انتخابشده متصل میکند. تطبیق خودکار گرههای نزدیک پردازنده گرافیکی را انتخاب میکند.',
|
||||
'انتخاب GPU دستگاههای مورد استفاده را تعیین میکند؛ وابستگی، گرههای CPU و حافظه را مشخص میکند.',
|
||||
numaRequirements:
|
||||
'تصویر باید numactl داشته باشد. با فعالسازی اتصال، مجوز SYS_NICE در صورت نیاز خودکار اضافه میشود.',
|
||||
numaUnknown:
|
||||
@@ -1259,8 +1275,10 @@ const message = {
|
||||
ascendA3: 'نسخه رسمی Ascend A3 برای دستگاههای سری Ascend A3.',
|
||||
ascendA5: 'نسخه رسمی Ascend A5 برای دستگاههای 950DT.',
|
||||
ascend310P: 'نسخه رسمی Ascend 310P برای دستگاههای Atlas 300I DUO و Atlas 200I Pro.',
|
||||
default: 'نسخه استاندارد رسمی با CUDA 13.0، مناسب درایورهای جدید و معماریهای جدید GPU.',
|
||||
dspark: 'نسخه سفارشی NVIDIA GB10 برای دو دستگاه، با ایمیج سفارشی که توسط 1Panel نگهداری میشود.',
|
||||
default: 'نسخه استاندارد رسمی جامعه vLLM با CUDA 13.0، برای درایورها و معماریهای جدیدتر کارت گرافیک.',
|
||||
rtxPro5000:
|
||||
'نسخه سفارشی برای NVIDIA RTX Pro 5000، بهینهشده برای DeepSeek-V4.1-Flash-1017. ایمیج توسط 1Panel نگهداری میشود.',
|
||||
dspark: 'نسخه سفارشی برای دو دستگاه NVIDIA GB10، بهینهشده برای DeepSeek-V4-Flash. ایمیج توسط 1Panel نگهداری میشود.',
|
||||
},
|
||||
vllmDeployProfile: 'مدل استقرار',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -776,6 +776,20 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate:
|
||||
'NUMA {node} に利用可能な GPU が検出されなかったため、このノードからグループを作成できません。',
|
||||
inspectNUMA:
|
||||
'現在のノードの端末で次の読み取り専用コマンドを実行し、PCI アドレスを照合して GPU の NUMA 所属を確認してください:',
|
||||
numaQueryHint:
|
||||
'numa_node は NUMA ノード番号を示します。-1 はシステムが所属を報告していないことを意味します。結果が画面と異なる場合は、更新して再試行してください。',
|
||||
relationTopology: '関係トポロジー',
|
||||
relationHelp: 'GPU またはグループを選択すると、所属関係が強調表示されます。',
|
||||
selectionHelp:
|
||||
'同じデバイス種類の GPU を選択し、次へ進んでグループを設定します。破線は未保存の所属関係を示します。',
|
||||
allRelations: 'すべての関係を表示',
|
||||
pendingChanges: '未保存',
|
||||
hardware: 'ハードウェア配置',
|
||||
editMembers: 'メンバーを編集',
|
||||
usageScope:
|
||||
'関連情報は、このノードで 1Panel が管理するアプリの設定に基づきます。メモリ値は GPU 全体の測定値です。',
|
||||
usageTitle: 'GPU 使用状況',
|
||||
@@ -1108,9 +1122,11 @@ const message = {
|
||||
numaAuto: '自動',
|
||||
numaManual: '手動',
|
||||
numaNodes: 'NUMA ノード',
|
||||
numaHint: '通常は「バインドなし」にし、性能調整が必要な場合に有効にしてください。',
|
||||
numaHelp:
|
||||
'アプリを指定した CPU とメモリのノードにバインドします。自動設定では GPU に近いノードを選択します。',
|
||||
numaOffHelp: '選択した GPU を使用し、CPU とメモリの割り当てはシステムに任せます。',
|
||||
numaAutoHelp: 'CPU とメモリを、選択した GPU が属する NUMA ノードにバインドします。',
|
||||
numaManualHelp: 'GPU の選択を変えずに、アプリが使用する CPU とメモリのノードを指定します。',
|
||||
numaResolved: 'NUMA {node} にバインドします。',
|
||||
numaHelp: 'GPU の選択は使用する GPU を決め、親和性は CPU とメモリに使用するノードを決めます。',
|
||||
numaRequirements:
|
||||
'イメージに numactl が必要です。バインドを有効にすると、必要に応じて SYS_NICE 権限が自動追加されます。',
|
||||
numaUnknown:
|
||||
@@ -1265,8 +1281,10 @@ const message = {
|
||||
ascendA5: '950DT 向けの Ascend 公式 A5 版です。',
|
||||
ascend310P: 'Atlas 300I DUO および Atlas 200I Pro 向けの Ascend 公式 310P 版です。',
|
||||
default:
|
||||
'公式の標準バージョン。CUDA 13.0 を使用し、新しいドライバーと新しい GPU アーキテクチャに対応します。',
|
||||
dspark: 'NVIDIA GB10 の 2 台構成向けカスタム版。1Panel が独自に保守するカスタムイメージです。',
|
||||
'vLLM 公式コミュニティの標準版。CUDA 13.0 を使用し、新しいドライバーと新しい GPU アーキテクチャに対応します。',
|
||||
rtxPro5000:
|
||||
'NVIDIA RTX Pro 5000 向けカスタム版。DeepSeek-V4.1-Flash-1017 向けに最適化されており、イメージは 1Panel が保守しています。',
|
||||
dspark: 'NVIDIA GB10 の 2 台構成向けカスタム版。DeepSeek-V4-Flash 向けに最適化されており、イメージは 1Panel が保守しています。',
|
||||
},
|
||||
vllmDeployProfile: 'デプロイモデル',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -767,6 +767,20 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate:
|
||||
'NUMA {node}에서 사용 가능한 GPU를 찾지 못했습니다. 이 노드에서 그룹을 생성할 수 없습니다.',
|
||||
inspectNUMA:
|
||||
'현재 노드의 터미널에서 다음 읽기 전용 명령을 실행하고 PCI 주소를 대조하여 GPU의 NUMA 소속을 확인하세요:',
|
||||
numaQueryHint:
|
||||
'numa_node 값은 NUMA 노드 번호입니다. -1은 시스템이 소속 정보를 보고하지 않았음을 나타냅니다. 결과가 페이지와 다르면 새로 고침 후 다시 시도하세요.',
|
||||
relationTopology: '관계 토폴로지',
|
||||
relationHelp: 'GPU 또는 그룹을 선택하면 소속 관계가 강조 표시됩니다.',
|
||||
selectionHelp:
|
||||
'동일한 장치 유형의 GPU를 선택한 후 다음 단계에서 그룹을 설정하세요. 점선은 저장되지 않은 소속 관계를 나타냅니다.',
|
||||
allRelations: '모든 관계 표시',
|
||||
pendingChanges: '저장되지 않음',
|
||||
hardware: '하드웨어 배치',
|
||||
editMembers: '구성원 편집',
|
||||
usageScope:
|
||||
'연결 정보는 이 노드에서 1Panel이 관리하는 앱 설정을 기준으로 합니다. 메모리는 전체 GPU의 측정값입니다.',
|
||||
usageTitle: 'GPU 사용 현황',
|
||||
@@ -1096,8 +1110,11 @@ const message = {
|
||||
numaAuto: '자동 매칭',
|
||||
numaManual: '수동 선택',
|
||||
numaNodes: 'NUMA 노드',
|
||||
numaHint: '일반적으로 바인딩하지 않고, 성능 조정이 필요할 때 활성화하세요.',
|
||||
numaHelp: '앱을 지정한 CPU와 메모리 노드에 바인딩합니다. 자동 매칭은 GPU 근처의 노드를 선택합니다.',
|
||||
numaOffHelp: '선택한 GPU를 사용하고 CPU와 메모리는 시스템이 관리합니다.',
|
||||
numaAutoHelp: '선택한 GPU가 속한 NUMA 노드에 CPU와 메모리를 바인딩합니다.',
|
||||
numaManualHelp: 'GPU 선택을 변경하지 않고 앱이 사용할 CPU와 메모리 노드를 지정합니다.',
|
||||
numaResolved: 'NUMA {node}에 바인딩됩니다.',
|
||||
numaHelp: 'GPU 선택은 사용할 GPU를 결정하고, 선호도는 CPU와 메모리에 사용할 노드를 결정합니다.',
|
||||
numaRequirements:
|
||||
'이미지에 numactl이 필요합니다. 바인딩을 활성화하면 필요한 경우 SYS_NICE 권한이 자동으로 추가됩니다.',
|
||||
numaUnknown:
|
||||
@@ -1249,8 +1266,11 @@ const message = {
|
||||
ascendA3: 'Ascend A3 시리즈 장치용 Ascend 공식 A3 버전입니다.',
|
||||
ascendA5: '950DT 장치용 Ascend 공식 A5 버전입니다.',
|
||||
ascend310P: 'Atlas 300I DUO 및 Atlas 200I Pro 장치용 Ascend 공식 310P 버전입니다.',
|
||||
default: '공식 표준 버전으로, CUDA 13.0을 사용하며 최신 드라이버와 새로운 GPU 아키텍처에 적합합니다.',
|
||||
dspark: 'NVIDIA GB10 장치 2대 구성용 맞춤 버전으로, 1Panel이 자체 유지 관리하는 맞춤 이미지입니다.',
|
||||
default:
|
||||
'vLLM 공식 커뮤니티 표준 버전으로, CUDA 13.0을 사용하며 최신 드라이버와 최신 GPU 아키텍처에 적합합니다.',
|
||||
rtxPro5000:
|
||||
'NVIDIA RTX Pro 5000용 맞춤 버전으로, DeepSeek-V4.1-Flash-1017에 최적화되어 있습니다. 이미지는 1Panel이 유지 관리합니다.',
|
||||
dspark: 'NVIDIA GB10 장치 2대 구성용 맞춤 버전으로, DeepSeek-V4-Flash에 최적화되어 있습니다. 이미지는 1Panel이 유지 관리합니다.',
|
||||
},
|
||||
vllmDeployProfile: '배포 모델',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -766,6 +766,19 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate: 'ບໍ່ພົບ GPU ທີ່ໃຊ້ໄດ້ໃນ NUMA {node}. ບໍ່ສາມາດສ້າງກຸ່ມຈາກໂນດນີ້ໄດ້.',
|
||||
inspectNUMA:
|
||||
'ໃຫ້ເອີ້ນໃຊ້ຄຳສັ່ງອ່ານຢ່າງດຽວນີ້ໃນເທີມິນອລຂອງໂນດປັດຈຸບັນ ແລະ ທຽບທີ່ຢູ່ PCI ເພື່ອກວດສອບ NUMA ຂອງ GPU:',
|
||||
numaQueryHint:
|
||||
'ຄ່າ numa_node ແມ່ນໝາຍເລກໂນດ NUMA; -1 ໝາຍເຖິງລະບົບບໍ່ໄດ້ລາຍງານການສັງກັດ. ຖ້າຜົນບໍ່ກົງກັບໜ້ານີ້ ໃຫ້ໂຫຼດໃໝ່ແລ້ວລອງອີກຄັ້ງ.',
|
||||
relationTopology: 'ໂຄງສ້າງຄວາມສຳພັນ',
|
||||
relationHelp: 'ເລືອກ GPU ຫຼື ກຸ່ມເພື່ອເນັ້ນຄວາມສຳພັນຂອງສະມາຊິກ.',
|
||||
selectionHelp:
|
||||
'ເລືອກ GPU ປະເພດອຸປະກອນດຽວກັນ ແລ້ວໄປຂັ້ນຕໍ່ໄປເພື່ອຕັ້ງຄ່າກຸ່ມ. ເສັ້ນປະໝາຍເຖິງສະມາຊິກທີ່ຍັງບໍ່ໄດ້ບັນທຶກ.',
|
||||
allRelations: 'ສະແດງຄວາມສຳພັນທັງໝົດ',
|
||||
pendingChanges: 'ຍັງບໍ່ໄດ້ບັນທຶກ',
|
||||
hardware: 'ຕຳແໜ່ງຮາດແວ',
|
||||
editMembers: 'ແກ້ໄຂສະມາຊິກ',
|
||||
usageScope:
|
||||
'ຂໍ້ມູນທີ່ກ່ຽວຂ້ອງມາຈາກການຕັ້ງຄ່າແອັບທີ່ 1Panel ຈັດການໃນໂນດນີ້; ຄ່າໜ່ວຍຄວາມຈຳແມ່ນວັດຈາກ GPU ທັງໝົດ.',
|
||||
usageTitle: 'ການໃຊ້ GPU',
|
||||
@@ -1092,8 +1105,11 @@ const message = {
|
||||
numaAuto: 'ອັດຕະໂນມັດ',
|
||||
numaManual: 'ເລືອກເອງ',
|
||||
numaNodes: 'ໂນດ NUMA',
|
||||
numaHint: 'ປິດການຜູກໄວ້ ແລະ ເປີດເມື່ອຕ້ອງການປັບປະສິດທິພາບ.',
|
||||
numaHelp: 'ຜູກແອັບກັບໂນດ CPU ແລະ ໜ່ວຍຄວາມຈຳທີ່ເລືອກ. ການຈັບຄູ່ອັດຕະໂນມັດຈະເລືອກໂນດໃກ້ GPU.',
|
||||
numaOffHelp: 'ໃຊ້ GPU ທີ່ເລືອກ ໂດຍໃຫ້ລະບົບຈັດການ CPU ແລະ ໜ່ວຍຄວາມຈຳ.',
|
||||
numaAutoHelp: 'ຜູກ CPU ແລະ ໜ່ວຍຄວາມຈຳກັບໂນດ NUMA ຂອງ GPU ທີ່ເລືອກ.',
|
||||
numaManualHelp: 'ກຳນົດໂນດ CPU ແລະ ໜ່ວຍຄວາມຈຳຂອງແອັບ ໂດຍບໍ່ປ່ຽນ GPU ທີ່ເລືອກ.',
|
||||
numaResolved: 'ຈະຜູກກັບ NUMA {node}.',
|
||||
numaHelp: 'ການເລືອກ GPU ກຳນົດ GPU ທີ່ໃຊ້; ນະໂຍບາຍຄວາມໃກ້ຊິດກຳນົດໂນດ CPU ແລະ ໜ່ວຍຄວາມຈຳ.',
|
||||
numaRequirements: 'ອິມເມຈຕ້ອງມີ numactl. ເມື່ອເປີດການຜູກ ຈະເພີ່ມສິດ SYS_NICE ອັດຕະໂນມັດຖ້າຈຳເປັນ.',
|
||||
numaUnknown:
|
||||
'ບໍ່ສາມາດລະບຸໂນດ CPU ແລະ ໜ່ວຍຄວາມຈຳທີ່ກ່ຽວຂ້ອງກັບ GPU. ສາມາດປິດການຜູກໄວ້ ຫຼື ເລືອກໂນດເອງຖ້າຮູ້ໂຄງສ້າງຮາດແວ.',
|
||||
@@ -1241,8 +1257,11 @@ const message = {
|
||||
ascendA3: 'ເວີຊັນ Ascend A3 ທາງການ ສຳລັບອຸປະກອນຊຸດ Ascend A3.',
|
||||
ascendA5: 'ເວີຊັນ Ascend A5 ທາງການ ສຳລັບອຸປະກອນ 950DT.',
|
||||
ascend310P: 'ເວີຊັນ Ascend 310P ທາງການ ສຳລັບອຸປະກອນ Atlas 300I DUO ແລະ Atlas 200I Pro.',
|
||||
default: 'ເວີຊັນມາດຕະຖານທາງການທີ່ໃຊ້ CUDA 13.0, ເໝາະສຳລັບໄດຣເວີໃໝ່ ແລະ GPU ສະຖາປັດຕະຍະກຳໃໝ່.',
|
||||
dspark: 'ເວີຊັນປັບແຕ່ງ NVIDIA GB10 ສຳລັບສອງອຸປະກອນ, ໃຊ້ອິມເມຈປັບແຕ່ງທີ່ 1Panel ດູແລເອງ.',
|
||||
default:
|
||||
'ເວີຊັນມາດຕະຖານທາງການຂອງຊຸມຊົນ vLLM ພ້ອມ CUDA 13.0, ເໝາະສຳລັບໄດຣເວີ ແລະ ສະຖາປັດຕະຍະກຳ GPU ລຸ້ນໃໝ່.',
|
||||
rtxPro5000:
|
||||
'ເວີຊັນປັບແຕ່ງສຳລັບ NVIDIA RTX Pro 5000, ປັບໃຫ້ເໝາະກັບ DeepSeek-V4.1-Flash-1017. ອິມເມຈດູແລໂດຍ 1Panel.',
|
||||
dspark: 'ເວີຊັນປັບແຕ່ງສຳລັບ NVIDIA GB10 ສອງອຸປະກອນ, ປັບໃຫ້ເໝາະກັບ DeepSeek-V4-Flash. ອິມເມຈດູແລໂດຍ 1Panel.',
|
||||
},
|
||||
vllmDeployProfile: 'ໂມເດວສຳລັບຕິດຕັ້ງ',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -785,6 +785,20 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate:
|
||||
'Tiada GPU tersedia dikesan pada NUMA {node}. Kumpulan tidak boleh dicipta daripada nod ini.',
|
||||
inspectNUMA:
|
||||
'Jalankan arahan baca sahaja ini dalam terminal nod semasa dan padankan alamat PCI untuk menyemak penempatan NUMA GPU:',
|
||||
numaQueryHint:
|
||||
'numa_node mengandungi ID nod NUMA; -1 bermaksud sistem belum melaporkan penempatan. Jika keputusan berbeza daripada halaman ini, muat semula dan cuba lagi.',
|
||||
relationTopology: 'Topologi hubungan',
|
||||
relationHelp: 'Pilih GPU atau kumpulan untuk menyerlahkan hubungan keahliannya.',
|
||||
selectionHelp:
|
||||
'Pilih GPU daripada jenis peranti yang sama, kemudian teruskan untuk mengkonfigurasi kumpulan. Garis putus-putus menunjukkan keahlian yang belum disimpan.',
|
||||
allRelations: 'Tunjukkan semua hubungan',
|
||||
pendingChanges: 'Belum disimpan',
|
||||
hardware: 'Penempatan perkakasan',
|
||||
editMembers: 'Sunting ahli',
|
||||
usageScope:
|
||||
'Perkaitan berdasarkan konfigurasi aplikasi yang diurus oleh 1Panel pada nod ini; nilai memori diukur untuk seluruh GPU.',
|
||||
usageTitle: 'Penggunaan GPU',
|
||||
@@ -1132,9 +1146,11 @@ const message = {
|
||||
numaAuto: 'Automatik',
|
||||
numaManual: 'Manual',
|
||||
numaNodes: 'Nod NUMA',
|
||||
numaHint: 'Biarkan pengikatan dimatikan kecuali untuk penalaan prestasi.',
|
||||
numaHelp:
|
||||
'Mengikat aplikasi kepada nod CPU dan memori yang dipilih. Padanan automatik memilih nod berhampiran GPU.',
|
||||
numaOffHelp: 'Gunakan GPU yang dipilih sementara sistem mengurus CPU dan memori.',
|
||||
numaAutoHelp: 'Ikat CPU dan memori pada nod NUMA yang menempatkan GPU terpilih.',
|
||||
numaManualHelp: 'Tentukan nod CPU dan memori aplikasi tanpa mengubah pilihan GPU.',
|
||||
numaResolved: 'Akan diikat pada NUMA {node}.',
|
||||
numaHelp: 'Pilihan GPU menentukan GPU yang digunakan; afiniti menentukan nod CPU dan memori.',
|
||||
numaRequirements:
|
||||
'Imej mesti mengandungi numactl. Mengaktifkan pengikatan menambah SYS_NICE secara automatik jika perlu.',
|
||||
numaUnknown:
|
||||
@@ -1294,8 +1310,11 @@ const message = {
|
||||
ascendA3: 'Versi rasmi Ascend A3 untuk peranti siri Ascend A3.',
|
||||
ascendA5: 'Versi rasmi Ascend A5 untuk peranti 950DT.',
|
||||
ascend310P: 'Versi rasmi Ascend 310P untuk peranti Atlas 300I DUO dan Atlas 200I Pro.',
|
||||
default: 'Versi standard rasmi dengan CUDA 13.0, untuk pemacu baharu dan seni bina GPU baharu.',
|
||||
dspark: 'Versi tersuai NVIDIA GB10 untuk dua peranti, menggunakan imej tersuai yang diselenggara oleh 1Panel.',
|
||||
default:
|
||||
'Versi standard komuniti rasmi vLLM dengan CUDA 13.0, sesuai untuk pemacu dan seni bina GPU yang lebih baharu.',
|
||||
rtxPro5000:
|
||||
'Versi tersuai untuk NVIDIA RTX Pro 5000, dioptimumkan untuk DeepSeek-V4.1-Flash-1017. Imej diselenggara oleh 1Panel.',
|
||||
dspark: 'Versi tersuai untuk dua peranti NVIDIA GB10, dioptimumkan untuk DeepSeek-V4-Flash. Imej diselenggara oleh 1Panel.',
|
||||
},
|
||||
vllmDeployProfile: 'Model penggunaan',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -778,6 +778,20 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate:
|
||||
'Nenhuma GPU disponível foi detectada no NUMA {node}. Não é possível criar um grupo a partir deste nó.',
|
||||
inspectNUMA:
|
||||
'Execute estes comandos somente de leitura no terminal do nó atual e compare os endereços PCI para verificar a localização NUMA das GPUs:',
|
||||
numaQueryHint:
|
||||
'numa_node contém o ID do nó NUMA; -1 indica que o sistema não informou a localização. Se os resultados diferirem desta página, atualize e tente novamente.',
|
||||
relationTopology: 'Topologia de relações',
|
||||
relationHelp: 'Selecione uma GPU ou um grupo para destacar suas relações de associação.',
|
||||
selectionHelp:
|
||||
'Selecione GPUs do mesmo tipo de dispositivo e avance para configurar o grupo. Linhas tracejadas indicam associações não salvas.',
|
||||
allRelations: 'Mostrar todas as relações',
|
||||
pendingChanges: 'Não salvo',
|
||||
hardware: 'Localização do hardware',
|
||||
editMembers: 'Editar membros',
|
||||
usageScope:
|
||||
'As associações vêm das configurações de aplicativos gerenciados pelo 1Panel neste nó; a memória é medida para toda a GPU.',
|
||||
usageTitle: 'Uso de GPU',
|
||||
@@ -1127,9 +1141,11 @@ const message = {
|
||||
numaAuto: 'Automática',
|
||||
numaManual: 'Manual',
|
||||
numaNodes: 'Nós NUMA',
|
||||
numaHint: 'Deixe a afinidade desativada, salvo para ajustar o desempenho.',
|
||||
numaHelp:
|
||||
'Vincula o aplicativo aos nós de CPU e memória selecionados. A seleção automática escolhe nós próximos às GPUs.',
|
||||
numaOffHelp: 'Usa as GPUs selecionadas enquanto o sistema gerencia CPU e memória.',
|
||||
numaAutoHelp: 'Vincula CPU e memória ao nó NUMA das GPUs selecionadas.',
|
||||
numaManualHelp: 'Especifica os nós de CPU e memória sem alterar a seleção de GPU.',
|
||||
numaResolved: 'Será vinculado ao NUMA {node}.',
|
||||
numaHelp: 'A seleção de GPU define quais GPUs usar; a afinidade define os nós de CPU e memória.',
|
||||
numaRequirements:
|
||||
'A imagem deve incluir numactl. Ativar a afinidade adiciona SYS_NICE automaticamente, se necessário.',
|
||||
numaUnknown:
|
||||
@@ -1289,8 +1305,11 @@ const message = {
|
||||
ascendA3: 'Versão oficial Ascend A3 para dispositivos da série Ascend A3.',
|
||||
ascendA5: 'Versão oficial Ascend A5 para dispositivos 950DT.',
|
||||
ascend310P: 'Versão oficial Ascend 310P para dispositivos Atlas 300I DUO e Atlas 200I Pro.',
|
||||
default: 'Versão padrão oficial com CUDA 13.0, para drivers recentes e GPUs de novas arquiteturas.',
|
||||
dspark: 'Versão personalizada do NVIDIA GB10 para dois dispositivos, com uma imagem mantida pelo 1Panel.',
|
||||
default:
|
||||
'Versão padrão oficial da comunidade vLLM com CUDA 13.0, para drivers e arquiteturas de GPU mais recentes.',
|
||||
rtxPro5000:
|
||||
'Versão personalizada para NVIDIA RTX Pro 5000, otimizada para DeepSeek-V4.1-Flash-1017. A imagem é mantida pelo 1Panel.',
|
||||
dspark: 'Versão personalizada para dois dispositivos NVIDIA GB10, otimizada para DeepSeek-V4-Flash. A imagem é mantida pelo 1Panel.',
|
||||
},
|
||||
vllmDeployProfile: 'Modelo de implantação',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -775,6 +775,19 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate: 'В NUMA {node} не обнаружены доступные GPU. Создать группу из этого узла невозможно.',
|
||||
inspectNUMA:
|
||||
'Выполните эти команды только для чтения в терминале текущего узла и сопоставьте адреса PCI, чтобы проверить принадлежность GPU к NUMA:',
|
||||
numaQueryHint:
|
||||
'numa_node содержит номер узла NUMA; -1 означает, что система не сообщила принадлежность. Если результаты отличаются от страницы, обновите её и повторите попытку.',
|
||||
relationTopology: 'Топология связей',
|
||||
relationHelp: 'Выберите GPU или группу, чтобы выделить связи участия.',
|
||||
selectionHelp:
|
||||
'Выберите GPU одного типа устройства и перейдите к настройке группы. Пунктирные линии обозначают несохранённые связи.',
|
||||
allRelations: 'Показать все связи',
|
||||
pendingChanges: 'Не сохранено',
|
||||
hardware: 'Размещение оборудования',
|
||||
editMembers: 'Изменить состав',
|
||||
usageScope:
|
||||
'Связи определяются конфигурациями приложений, управляемых 1Panel на этом узле; память измеряется для всего GPU.',
|
||||
usageTitle: 'Использование GPU',
|
||||
@@ -1120,9 +1133,11 @@ const message = {
|
||||
numaAuto: 'Автоматически',
|
||||
numaManual: 'Вручную',
|
||||
numaNodes: 'Узлы NUMA',
|
||||
numaHint: 'Оставьте привязку отключённой, если не требуется настройка производительности.',
|
||||
numaHelp:
|
||||
'Привязывает приложение к выбранным узлам процессоров и памяти. Автоматический выбор находит узлы рядом с GPU.',
|
||||
numaOffHelp: 'Используются выбранные GPU, а CPU и память распределяются системой.',
|
||||
numaAutoHelp: 'CPU и память привязываются к узлу NUMA выбранных GPU.',
|
||||
numaManualHelp: 'Укажите узлы CPU и памяти для приложения, не меняя выбор GPU.',
|
||||
numaResolved: 'Будет выполнена привязка к NUMA {node}.',
|
||||
numaHelp: 'Выбор GPU определяет используемые видеоустройства, а привязка — узлы CPU и памяти.',
|
||||
numaRequirements:
|
||||
'Образ должен содержать numactl. При включении привязки при необходимости автоматически добавляется SYS_NICE.',
|
||||
numaUnknown:
|
||||
@@ -1281,8 +1296,10 @@ const message = {
|
||||
ascendA5: 'Официальная версия Ascend A5 для устройств 950DT.',
|
||||
ascend310P: 'Официальная версия Ascend 310P для устройств Atlas 300I DUO и Atlas 200I Pro.',
|
||||
default:
|
||||
'Официальная стандартная версия с CUDA 13.0 для новых драйверов и видеокарт с новой архитектурой.',
|
||||
dspark: 'Специальная версия NVIDIA GB10 для двух устройств с образом, поддерживаемым 1Panel.',
|
||||
'Официальная стандартная версия сообщества vLLM с CUDA 13.0 для новых драйверов и новых архитектур GPU.',
|
||||
rtxPro5000:
|
||||
'Специальная версия для NVIDIA RTX Pro 5000, оптимизированная для DeepSeek-V4.1-Flash-1017. Образ поддерживается 1Panel.',
|
||||
dspark: 'Специальная версия для двух устройств NVIDIA GB10, оптимизированная для DeepSeek-V4-Flash. Образ поддерживается 1Panel.',
|
||||
},
|
||||
vllmDeployProfile: 'Модель развертывания',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -781,6 +781,19 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate: 'NUMA {node} altında kullanılabilir GPU algılanmadı. Bu düğümden grup oluşturulamaz.',
|
||||
inspectNUMA:
|
||||
'GPU NUMA yerleşimini kontrol etmek için mevcut düğümün terminalinde bu salt okunur komutları çalıştırın ve PCI adreslerini eşleştirin:',
|
||||
numaQueryHint:
|
||||
'numa_node, NUMA düğüm kimliğini içerir; -1, sistemin yerleşimi bildirmediği anlamına gelir. Sonuçlar bu sayfadan farklıysa yenileyip tekrar deneyin.',
|
||||
relationTopology: 'İlişki topolojisi',
|
||||
relationHelp: 'Üyelik ilişkilerini vurgulamak için bir GPU veya grup seçin.',
|
||||
selectionHelp:
|
||||
'Aynı aygıt türündeki GPU’ları seçin ve grubu yapılandırmak için ilerleyin. Kesik çizgiler kaydedilmemiş üyelikleri gösterir.',
|
||||
allRelations: 'Tüm ilişkileri göster',
|
||||
pendingChanges: 'Kaydedilmedi',
|
||||
hardware: 'Donanım yerleşimi',
|
||||
editMembers: 'Üyeleri düzenle',
|
||||
usageScope:
|
||||
'İlişkiler, bu düğümde 1Panel tarafından yönetilen uygulama yapılandırmalarından alınır; bellek değerleri GPU’nun tamamı için ölçülür.',
|
||||
usageTitle: 'GPU kullanımı',
|
||||
@@ -1129,9 +1142,11 @@ const message = {
|
||||
numaAuto: 'Otomatik',
|
||||
numaManual: 'Elle',
|
||||
numaNodes: 'NUMA düğümleri',
|
||||
numaHint: 'Performans ayarı gerekmiyorsa bağlamayı kapalı bırakın.',
|
||||
numaHelp:
|
||||
'Uygulamayı seçilen CPU ve bellek düğümlerine bağlar. Otomatik eşleştirme GPU yakınındaki düğümleri seçer.',
|
||||
numaOffHelp: 'Seçilen GPU’lar kullanılır; CPU ve bellek sistem tarafından yönetilir.',
|
||||
numaAutoHelp: 'CPU ve bellek, seçilen GPU’ların bulunduğu NUMA düğümüne bağlanır.',
|
||||
numaManualHelp: 'GPU seçimini değiştirmeden uygulamanın CPU ve bellek düğümlerini belirtin.',
|
||||
numaResolved: 'NUMA {node} düğümüne bağlanacak.',
|
||||
numaHelp: 'GPU seçimi kullanılacak GPU’ları, yakınlık ise CPU ve bellek düğümlerini belirler.',
|
||||
numaRequirements:
|
||||
'İmaj numactl içermelidir. Bağlama etkinleştirildiğinde gerekirse SYS_NICE otomatik eklenir.',
|
||||
numaUnknown:
|
||||
@@ -1288,8 +1303,11 @@ const message = {
|
||||
ascendA3: 'Ascend A3 serisi cihazlar için resmi Ascend A3 sürümü.',
|
||||
ascendA5: '950DT cihazları için resmi Ascend A5 sürümü.',
|
||||
ascend310P: 'Atlas 300I DUO ve Atlas 200I Pro cihazları için resmi Ascend 310P sürümü.',
|
||||
default: 'Yeni sürücüler ve yeni GPU mimarileri için CUDA 13.0 içeren resmi standart sürüm.',
|
||||
dspark: '1Panel tarafından bakımı yapılan özel imajı kullanan, iki cihaz için NVIDIA GB10 özel sürümü.',
|
||||
default:
|
||||
'Yeni sürücüler ve yeni GPU mimarileri için CUDA 13.0 içeren resmî vLLM topluluk standart sürümü.',
|
||||
rtxPro5000:
|
||||
'NVIDIA RTX Pro 5000 için DeepSeek-V4.1-Flash-1017’ye optimize edilmiş özel sürüm. İmajın bakımı 1Panel tarafından yapılır.',
|
||||
dspark: 'İki NVIDIA GB10 cihazı için DeepSeek-V4-Flash’a optimize edilmiş özel sürüm. İmajın bakımı 1Panel tarafından yapılır.',
|
||||
},
|
||||
vllmDeployProfile: 'Dağıtım modeli',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -743,6 +743,17 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate: 'NUMA {node} 下未偵測到可用的 GPU,無法從此節點建立分組。',
|
||||
inspectNUMA: '請在目前節點的終端執行以下唯讀命令,按 PCI 位址對照 GPU 與 NUMA 歸屬:',
|
||||
numaQueryHint:
|
||||
'numa_node 的值是 NUMA 節點編號;-1 表示系統未回報歸屬。若偵測結果與頁面不一致,請重新整理後再試。',
|
||||
relationTopology: '關係拓撲',
|
||||
relationHelp: '點擊 GPU 或分組,醒目顯示成員關係。',
|
||||
selectionHelp: '選擇相同裝置類型的 GPU,點擊下一步設定分組;虛線表示待儲存的成員關係。',
|
||||
allRelations: '顯示全部關係',
|
||||
pendingChanges: '待儲存',
|
||||
hardware: '硬體歸屬',
|
||||
editMembers: '編輯成員',
|
||||
usageScope: '關聯資訊來自本節點由 1Panel 管理的應用程式設定;顯存為整卡取樣值。',
|
||||
usageTitle: 'GPU 使用情況',
|
||||
usageHelp: '所選 GPU 可能被其他部署使用。繼續部署可能導致顯存不足或效能下降,仍可繼續。',
|
||||
@@ -1053,8 +1064,11 @@ const message = {
|
||||
numaAuto: '自動匹配',
|
||||
numaManual: '手動選擇',
|
||||
numaNodes: 'NUMA 節點',
|
||||
numaHint: '一般保持「不綁定」,需要效能調校時再開啟。',
|
||||
numaHelp: '將應用綁定至指定的 CPU 和記憶體節點;自動匹配可選擇 GPU 附近的節點。',
|
||||
numaOffHelp: '使用所選 GPU,CPU 和記憶體由系統排程。',
|
||||
numaAutoHelp: 'CPU 和記憶體綁定到所選 GPU 所屬的 NUMA 節點。',
|
||||
numaManualHelp: '指定應用使用的 CPU 和記憶體節點,不改變 GPU 的選擇。',
|
||||
numaResolved: '將綁定 NUMA {node}。',
|
||||
numaHelp: 'GPU 裝置決定使用哪些顯示卡,親和策略決定 CPU 和記憶體使用哪些節點。',
|
||||
numaRequirements: '映像需包含 numactl;啟用綁定會自動設定 SYS_NICE 權限。',
|
||||
numaUnknown: '未能識別 GPU 對應的 CPU 和記憶體節點。可保持「不綁定」;了解硬體配置時再手動選擇。',
|
||||
numaCross: '所選 GPU 跨多個 NUMA 節點,請手動選擇。',
|
||||
@@ -1191,8 +1205,9 @@ const message = {
|
||||
ascendA3: 'Ascend 官方 A3 版本,適用於 Ascend A3 系列裝置。',
|
||||
ascendA5: 'Ascend 官方 A5 版本,適用於 950DT 裝置。',
|
||||
ascend310P: 'Ascend 官方 310P 版本,適用於 Atlas 300I DUO、Atlas 200I Pro 裝置。',
|
||||
default: '官方標準版本,CUDA 13.0,適用於新驅動程式和新架構顯示卡。',
|
||||
dspark: 'NVIDIA GB10 雙裝置客製版,由 1Panel 自行維護的客製映像。',
|
||||
default: 'vLLM 官方社群標準版本,CUDA 13.0,適用於新驅動程式和新架構顯示卡。',
|
||||
rtxPro5000: 'NVIDIA RTX Pro 5000 客製版,針對 DeepSeek-V4.1-Flash-1017 最佳化,映像由 1Panel 維護。',
|
||||
dspark: 'NVIDIA GB10 雙裝置客製版,針對 DeepSeek-V4-Flash 最佳化,映像由 1Panel 維護。',
|
||||
},
|
||||
vllmDeployProfile: '部署模型',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -760,6 +760,17 @@ const message = {
|
||||
},
|
||||
aiTools: {
|
||||
gpuGroup: {
|
||||
emptyNodeCreate: 'NUMA {node} 下未检测到可用的 GPU,无法从此节点创建分组。',
|
||||
inspectNUMA: '请在当前节点的终端执行以下只读命令,按 PCI 地址对照 GPU 与 NUMA 归属:',
|
||||
numaQueryHint:
|
||||
'numa_node 的值是 NUMA 节点编号;-1 表示系统未报告归属。若检测结果与页面不一致,请刷新后重试。',
|
||||
relationTopology: '关系拓扑',
|
||||
relationHelp: '点击 GPU 或分组,高亮成员关系。',
|
||||
selectionHelp: '选择同一设备类型的 GPU,点击下一步配置分组;虚线表示待保存的成员关系。',
|
||||
allRelations: '显示全部关系',
|
||||
pendingChanges: '待保存',
|
||||
hardware: '硬件归属',
|
||||
editMembers: '编辑成员',
|
||||
usageScope: '关联信息来自本节点由 1Panel 管理的应用配置;显存为整卡采样值。',
|
||||
usageTitle: 'GPU 使用情况',
|
||||
usageHelp: '所选 GPU 可能被其他部署使用。继续部署可能导致显存不足或性能下降,仍可继续。',
|
||||
@@ -1066,8 +1077,11 @@ const message = {
|
||||
numaAuto: '自动匹配',
|
||||
numaManual: '手动选择',
|
||||
numaNodes: 'NUMA 节点',
|
||||
numaHint: '一般保持“不绑定”,需要性能调优时再开启。',
|
||||
numaHelp: '将应用绑定到指定的 CPU 和内存节点;自动匹配可选择 GPU 附近的节点。',
|
||||
numaOffHelp: '使用所选 GPU,CPU 和内存由系统调度。',
|
||||
numaAutoHelp: 'CPU 和内存绑定到所选 GPU 所属的 NUMA 节点。',
|
||||
numaManualHelp: '指定应用使用的 CPU 和内存节点,不改变 GPU 的选择。',
|
||||
numaResolved: '将绑定 NUMA {node}。',
|
||||
numaHelp: 'GPU 设备决定使用哪些显卡,亲和策略决定 CPU 和内存使用哪些节点。',
|
||||
numaRequirements: '镜像需包含 numactl;启用绑定会自动配置 SYS_NICE 权限。',
|
||||
numaUnknown: '未能识别 GPU 对应的 CPU 和内存节点。可保持“不绑定”;了解硬件布局时再手动选择。',
|
||||
numaCross: '所选 GPU 跨多个 NUMA 节点,请手动选择。',
|
||||
@@ -1202,8 +1216,9 @@ const message = {
|
||||
ascendA3: 'Ascend 官方 A3 版本,适用于 Ascend A3 系列设备。',
|
||||
ascendA5: 'Ascend 官方 A5 版本,适用于 950DT 设备。',
|
||||
ascend310P: 'Ascend 官方 310P 版本,适用于 Atlas 300I DUO、Atlas 200I Pro 设备。',
|
||||
default: '官方标准版本,CUDA 13.0,适用于新驱动和新架构显卡。',
|
||||
dspark: 'NVIDIA GB10 双设备定制版,1Panel 自行维护的定制镜像。',
|
||||
default: 'vLLM官方社区标准版本,CUDA 13.0,适用于新驱动和新架构显卡。',
|
||||
rtxPro5000: 'NVIDIA RTX Pro 5000 定制版,针对 DeepSeek-V4.1-Flash-1017 优化,镜像由 1Panel 维护。',
|
||||
dspark: 'NVIDIA GB10 双设备定制版,针对 DeepSeek-V4-Flash 优化,镜像由 1Panel 维护。',
|
||||
},
|
||||
vllmDeployProfile: '部署模型',
|
||||
vllmProfileFlash0731: 'DeepSeek V4 Flash 0731',
|
||||
|
||||
@@ -17,7 +17,7 @@ const aiRouter = {
|
||||
name: 'GPUGroups',
|
||||
component: () => import('@/views/ai/gpu/groups/index.vue'),
|
||||
hidden: true,
|
||||
meta: { title: 'aiTools.gpuGroup.groups', activeMenu: '/ai/gpu/current', permission: 'ai_gpu_view' },
|
||||
meta: { title: 'website.groupSetting', activeMenu: '/ai/gpu/current', permission: 'ai_gpu_view' },
|
||||
},
|
||||
{
|
||||
path: '/ai/gpu/current',
|
||||
|
||||
@@ -35,13 +35,23 @@
|
||||
prop="gpu.group"
|
||||
:rules="groupRules"
|
||||
>
|
||||
<el-select :model-value="model.group?.id" class="w-full" @change="applyGroup">
|
||||
<el-select :model-value="model.group?.id" class="w-full" fit-input-width @change="applyGroup">
|
||||
<el-option
|
||||
v-for="group in compatibleGroups"
|
||||
:key="group.id"
|
||||
class="gpu-group-option"
|
||||
:value="group.id"
|
||||
:label="group.name"
|
||||
/>
|
||||
>
|
||||
<span class="gpu-group-option-name" :title="group.name">{{ group.name }}</span>
|
||||
<span
|
||||
v-if="group.description"
|
||||
class="gpu-group-option-description"
|
||||
:title="group.description"
|
||||
>
|
||||
{{ group.description }}
|
||||
</span>
|
||||
</el-option>
|
||||
<el-option
|
||||
v-if="model.group && !compatibleGroups.some((group) => group.id === model.group?.id)"
|
||||
:value="model.group.id"
|
||||
@@ -121,17 +131,25 @@
|
||||
prop="gpu.deviceIDs"
|
||||
:rules="gpuDeviceRules"
|
||||
>
|
||||
<template v-if="groupEditor" #label>
|
||||
<span class="group-device-label">
|
||||
{{ $t('aiTools.model.gpuDevices') }}
|
||||
<span>{{ $t('aiTools.gpuGroup.selectedGPU', { count: model.deviceIDs.length }) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<GPUTopology
|
||||
v-model="model.deviceIDs"
|
||||
:devices="(gpuDevices || []).filter((device) => device.type === imageType)"
|
||||
:nodes="numaNodes"
|
||||
:cards="groupEditor"
|
||||
cards
|
||||
selectable
|
||||
@update:model-value="model.error = undefined"
|
||||
>
|
||||
<template #device-meta="{ device }"><slot name="device-meta" :device="device" /></template>
|
||||
<template #device-meta="{ device }">
|
||||
<slot name="device-meta" :device="device" />
|
||||
</template>
|
||||
</GPUTopology>
|
||||
<span class="input-help w-full">
|
||||
<span v-if="!groupEditor" class="input-help w-full">
|
||||
{{ $t('aiTools.gpuGroup.selectedGPU', { count: model.deviceIDs.length }) }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
@@ -140,17 +158,14 @@
|
||||
:label="$t('aiTools.gpuGroup.affinity')"
|
||||
prop="gpu.numa.mode"
|
||||
:rules="numaRules"
|
||||
:show-message="model.numa.mode !== 'manual'"
|
||||
:show-message="false"
|
||||
>
|
||||
<template #label>
|
||||
<span class="gpu-capabilities-label">
|
||||
{{ $t('aiTools.gpuGroup.affinity') }}
|
||||
<el-tooltip placement="top">
|
||||
<template #content>
|
||||
<div class="gpu-capabilities-help">
|
||||
<div>{{ $t('aiTools.model.numaHelp') }}</div>
|
||||
<div class="mt-2">{{ $t('aiTools.model.numaHint') }}</div>
|
||||
</div>
|
||||
<div class="gpu-capabilities-help">{{ $t('aiTools.model.numaHelp') }}</div>
|
||||
</template>
|
||||
<el-icon tabindex="0" :aria-label="$t('aiTools.model.numaHelp')">
|
||||
<InfoFilled />
|
||||
@@ -165,6 +180,28 @@
|
||||
</el-radio-button>
|
||||
<el-radio-button value="manual">{{ $t('aiTools.model.numaManual') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="numa-strategy-help" aria-live="polite">
|
||||
<p class="input-help">
|
||||
{{
|
||||
$t(
|
||||
model.numa.mode === 'off'
|
||||
? 'aiTools.model.numaOffHelp'
|
||||
: model.numa.mode === 'auto'
|
||||
? 'aiTools.model.numaAutoHelp'
|
||||
: 'aiTools.model.numaManualHelp',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
v-if="model.numa.mode === 'auto' && !loading"
|
||||
:class="numaError ? 'numa-strategy-warning' : 'input-help'"
|
||||
>
|
||||
{{
|
||||
numaError ||
|
||||
$t('aiTools.model.numaResolved', { node: selectedNUMAGPUs[0]?.numaNode })
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="model.numa.mode === 'manual'"
|
||||
@@ -327,6 +364,9 @@ const numaError = computed(() => {
|
||||
let ids = config.nodes;
|
||||
if (config.mode === 'auto') {
|
||||
if (model.value.mode === 'count') return i18n.global.t('aiTools.model.numaCount');
|
||||
if (!gpuDevices.value) return i18n.global.t('aiTools.model.loadFailed');
|
||||
if (model.value.mode === 'devices' && !model.value.deviceIDs.length)
|
||||
return i18n.global.t('aiTools.model.gpuSelectRequired');
|
||||
const devices = selectedNUMAGPUs.value;
|
||||
if (
|
||||
!devices.length ||
|
||||
@@ -511,15 +551,57 @@ defineExpose({ init, loading });
|
||||
@use '@/components/form-section/index.scss';
|
||||
.group-editor {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
padding-top: 20px;
|
||||
background: transparent;
|
||||
}
|
||||
.group-editor > .form-section-body {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.group-device-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.group-device-label > span {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.group-editor :deep(.el-form-item__label) {
|
||||
width: 100%;
|
||||
}
|
||||
.group-editor .input-help {
|
||||
width: 100%;
|
||||
}
|
||||
.gpu-group-option {
|
||||
height: auto;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.gpu-group-option-name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
}
|
||||
.gpu-group-option-description {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
margin-top: 3px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
:deep(.el-form-item__error) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
@@ -541,4 +623,19 @@ defineExpose({ init, loading });
|
||||
max-width: 360px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.numa-strategy-help {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.numa-strategy-help p {
|
||||
margin: 0;
|
||||
}
|
||||
.numa-strategy-warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
v-for="section in sections"
|
||||
:key="section.id ?? 'unknown'"
|
||||
class="numa-node"
|
||||
:class="{ unknown: section.id === undefined }"
|
||||
:class="{ unknown: section.id === undefined, unassigned: cards && section.id === undefined }"
|
||||
>
|
||||
<header class="numa-node-header">
|
||||
<header v-if="section.id !== undefined || !cards" class="numa-node-header">
|
||||
<div>
|
||||
<strong>
|
||||
{{ section.id === undefined ? $t('aiTools.gpuGroup.unknownNode') : `NUMA ${section.id}` }}
|
||||
@@ -87,7 +87,7 @@ const sections = computed(() => {
|
||||
);
|
||||
const unknown = props.devices.filter((device) => !props.nodes.some((node) => node.id === device.numaNode));
|
||||
if (unknown.length) result.push({ devices: unknown });
|
||||
return result;
|
||||
return result.filter((section) => !props.selectable || section.devices.length > 0);
|
||||
});
|
||||
const missingDevices = computed(() => selected.value.filter((id) => !props.devices.some((device) => device.id === id)));
|
||||
const toggleDevice = (id: string, checked: boolean) => {
|
||||
@@ -108,15 +108,19 @@ const toggleNode = (devices: AI.ModelGPUDevice[], checked: boolean) => {
|
||||
gap: 24px;
|
||||
}
|
||||
.topology-cards {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(440px, 100%), 1fr));
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(380px, 100%), 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
.topology-cards > .numa-node {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
.gpu-topology.topology-cards > .numa-node.unassigned {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.topology-cards .numa-node-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -126,6 +130,33 @@ const toggleNode = (devices: AI.ModelGPUDevice[], checked: boolean) => {
|
||||
.topology-cards .topology-device {
|
||||
align-content: start;
|
||||
}
|
||||
.topology-cards .numa-devices {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
.topology-cards .topology-device {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.topology-cards .topology-device + .topology-device {
|
||||
border-top-color: var(--el-border-color-extra-light);
|
||||
}
|
||||
.topology-cards .topology-device.selected {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
.topology-cards.selectable .numa-devices {
|
||||
gap: 6px;
|
||||
}
|
||||
.topology-cards.selectable .unassigned .numa-devices {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.topology-cards.selectable .unassigned .topology-device:not(.selected) {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
}
|
||||
.topology-cards.selectable {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<div>
|
||||
<RouterMenu :history-disabled="historyDisabled" />
|
||||
<div>
|
||||
<LayoutContent v-loading="loading" :title="$t('aiTools.gpuGroup.overview')">
|
||||
<template #leftToolBar>
|
||||
<LayoutContent v-loading="loading" :title="$t('aiTools.gpu.current')">
|
||||
<template v-if="environmentStatus !== 'notDetected'" #leftToolBar>
|
||||
<div class="monitor-heading">
|
||||
<h3 class="environment-heading">{{ $t('aiTools.gpuGroup.overview') }}</h3>
|
||||
<h3 class="environment-heading">{{ $t('aiTools.gpu.current') }}</h3>
|
||||
<template v-if="loadFailed || gpuInfo.environment || gpuInfo.warnings?.length">
|
||||
<el-tag :type="environmentPassed ? 'success' : loadFailed ? 'danger' : 'warning'">
|
||||
{{
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<template v-if="environmentStatus !== 'notDetected'" #rightToolBar>
|
||||
<span v-if="hasAccelerators && gpuInfo.collectedAt" class="collection-time">
|
||||
{{ $t('aiTools.gpu.collectedAt') }}:
|
||||
{{ new Date(gpuInfo.collectedAt).toLocaleString() }}
|
||||
@@ -31,19 +31,6 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #main>
|
||||
<section class="topology-overview mb-4" v-loading="topologyLoading">
|
||||
<h3 class="environment-heading mb-3">{{ $t('aiTools.gpuGroup.topology') }}</h3>
|
||||
<el-alert
|
||||
v-if="topologyFailed"
|
||||
class="mb-3"
|
||||
:closable="false"
|
||||
type="warning"
|
||||
:title="$t('aiTools.model.loadFailed')"
|
||||
/>
|
||||
<GPUTopology v-if="!topologyFailed" :devices="topology.devices" :nodes="topology.numaNodes" />
|
||||
<div v-for="warning in topology.warnings" :key="warning" class="input-help">{{ warning }}</div>
|
||||
</section>
|
||||
|
||||
<div v-for="summary in overviewRows" :key="summary.vendor" class="device-overview mb-2">
|
||||
<div class="overview-item overview-model">
|
||||
<strong>{{ commonDeviceModels[summary.vendor] || vendorTitle(summary.vendor) }}</strong>
|
||||
@@ -597,9 +584,9 @@
|
||||
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Box, InfoFilled, Monitor } from '@element-plus/icons-vue';
|
||||
import { loadGPUInfo, loadModelGPUDevices } from '@/api/modules/ai';
|
||||
import { loadGPUInfo } from '@/api/modules/ai';
|
||||
import RouterMenu from '@/views/ai/gpu/index.vue';
|
||||
import GPUTopology from '@/views/ai/components/gpu-topology/index.vue';
|
||||
import { getGPUEnvironmentState } from '../environment';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { routerToName } from '@/utils/router';
|
||||
@@ -613,29 +600,6 @@ const pagePath = route.path;
|
||||
const { currentNode } = useGlobalStore();
|
||||
const loading = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
const topology = ref<{ devices: AI.ModelGPUDevice[]; numaNodes: AI.ModelNUMANode[]; warnings: string[] }>({
|
||||
devices: [],
|
||||
numaNodes: [],
|
||||
warnings: [],
|
||||
});
|
||||
const topologyLoading = ref(false);
|
||||
const topologyFailed = ref(false);
|
||||
let topologyRequest = 0;
|
||||
const loadTopology = async () => {
|
||||
if (route.path !== pagePath) return;
|
||||
const version = ++topologyRequest;
|
||||
const node = currentNode.value;
|
||||
topologyLoading.value = true;
|
||||
topologyFailed.value = false;
|
||||
try {
|
||||
const { data } = await loadModelGPUDevices(node);
|
||||
if (version === topologyRequest && node === currentNode.value) topology.value = data;
|
||||
} catch {
|
||||
if (version === topologyRequest) topologyFailed.value = true;
|
||||
} finally {
|
||||
if (version === topologyRequest) topologyLoading.value = false;
|
||||
}
|
||||
};
|
||||
const environmentDetails = ref<string[]>(['environment']);
|
||||
const environmentDialogVisible = ref(false);
|
||||
const processDrawerVisible = ref(false);
|
||||
@@ -900,23 +864,11 @@ const commonDeviceModels = computed<Record<string, string>>(() => {
|
||||
return models;
|
||||
});
|
||||
|
||||
const environmentChecks = computed(() =>
|
||||
(gpuInfo.value.environment?.checks || []).filter((check) => check.item === 'driver' || check.vendor === 'nvidia'),
|
||||
);
|
||||
const environmentIssueCount = computed(
|
||||
() =>
|
||||
environmentChecks.value.filter((check) => check.status !== 'passed').length +
|
||||
(gpuInfo.value.warnings?.length || 0),
|
||||
);
|
||||
const environmentPassed = computed(
|
||||
() => !loadFailed.value && environmentChecks.value.length > 0 && environmentIssueCount.value === 0,
|
||||
);
|
||||
const environmentStatus = computed(() => {
|
||||
if (loadFailed.value) return 'loadFailed';
|
||||
if (environmentPassed.value) return 'passed';
|
||||
if (environmentIssueCount.value) return 'issues';
|
||||
return gpuInfo.value.environment?.hardwareStatus === 'not_detected' ? 'notDetected' : 'unknown';
|
||||
});
|
||||
const environment = computed(() => getGPUEnvironmentState(gpuInfo.value, loadFailed.value));
|
||||
const environmentChecks = computed(() => environment.value.checks);
|
||||
const environmentIssueCount = computed(() => environment.value.issueCount);
|
||||
const environmentPassed = computed(() => environment.value.status === 'passed');
|
||||
const environmentStatus = computed(() => environment.value.status);
|
||||
const historyDisabled = computed(
|
||||
() =>
|
||||
(loading.value && !gpuInfo.value.environment && !hasAccelerators.value) ||
|
||||
@@ -1103,15 +1055,9 @@ const search = async (refreshEnvironment = false) => {
|
||||
|
||||
const refresh = () => {
|
||||
search(true);
|
||||
loadTopology();
|
||||
};
|
||||
|
||||
watch(currentNode, () => {
|
||||
topologyRequest++;
|
||||
topology.value = { devices: [], numaNodes: [], warnings: [] };
|
||||
topologyLoading.value = false;
|
||||
topologyFailed.value = false;
|
||||
loadTopology();
|
||||
environmentDialogVisible.value = false;
|
||||
environmentDetails.value = ['environment'];
|
||||
requestID++;
|
||||
@@ -1529,16 +1475,12 @@ const loadEcc = (val: string) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadTopology();
|
||||
search();
|
||||
});
|
||||
onActivated(() => {
|
||||
if (!topologyLoading.value) loadTopology();
|
||||
search();
|
||||
});
|
||||
const invalidateRequest = () => {
|
||||
topologyRequest++;
|
||||
topologyLoading.value = false;
|
||||
requestID++;
|
||||
loading.value = false;
|
||||
};
|
||||
@@ -1555,6 +1497,8 @@ onBeforeUnmount(invalidateRequest);
|
||||
}
|
||||
.environment-detail-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
.environment-detail-link:not(.is-disabled) {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.collection-time {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AI } from '@/api/interface/ai';
|
||||
|
||||
export const getGPUEnvironmentState = (info?: AI.Info, failed = false) => {
|
||||
const checks = (info?.environment?.checks || []).filter(
|
||||
(check) => check.item === 'driver' || check.vendor === 'nvidia',
|
||||
);
|
||||
const issueCount = checks.filter((check) => check.status !== 'passed').length + (info?.warnings?.length || 0);
|
||||
const status = failed
|
||||
? 'loadFailed'
|
||||
: checks.length > 0 && issueCount === 0
|
||||
? 'passed'
|
||||
: issueCount > 0
|
||||
? 'issues'
|
||||
: info?.environment?.hardwareStatus === 'not_detected'
|
||||
? 'notDetected'
|
||||
: 'unknown';
|
||||
return { checks, issueCount, status };
|
||||
};
|
||||
@@ -1,116 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterMenu />
|
||||
<LayoutContent v-loading="loading" :title="$t('aiTools.gpuGroup.groups')">
|
||||
<template #leftToolBar>
|
||||
<el-button v-permission="'ai_gpu_manage'" type="primary" @click="open()">
|
||||
<LayoutContent v-loading="loading" :title="$t('website.groupSetting')">
|
||||
<template v-if="!noDevices" #leftToolBar>
|
||||
<el-button v-permission="'ai_gpu_manage'" type="primary" :disabled="visible || loading" @click="open()">
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="viewMode" class="p-w-200">
|
||||
<template #prefix>{{ $t('aiTools.gpuGroup.dimension') }}</template>
|
||||
<el-option value="group" :label="$t('aiTools.gpuGroup.byGroup')" />
|
||||
<el-option value="numa" :label="$t('aiTools.gpuGroup.byNUMA')" />
|
||||
</el-select>
|
||||
<TableRefresh :disabled="loading" @search="load" />
|
||||
<template v-if="!noDevices" #rightToolBar>
|
||||
<TableRefresh :disabled="loading || visible" @search="load" />
|
||||
</template>
|
||||
<template #prompt>
|
||||
<el-alert :closable="false" :title="$t('aiTools.gpuGroup.help')" type="info" />
|
||||
<template v-if="!noDevices" #prompt>
|
||||
<p class="group-help">{{ $t('aiTools.gpuGroup.help') }}</p>
|
||||
</template>
|
||||
<template #main>
|
||||
<el-empty v-if="viewMode === 'group' && !groups.length && !loading" />
|
||||
<div v-if="viewMode === 'group'" class="gpu-group-cards">
|
||||
<el-card v-for="group in groups" :key="group.id" shadow="never" class="gpu-group-card">
|
||||
<template #header>
|
||||
<div class="group-card-header">
|
||||
<strong>{{ group.name }}</strong>
|
||||
<div class="group-card-actions">
|
||||
<el-button v-permission="'ai_gpu_manage'" link type="primary" @click="open(group)">
|
||||
{{ $t('commons.button.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission="'ai_gpu_manage'"
|
||||
link
|
||||
type="primary"
|
||||
@click="remove(group)"
|
||||
>
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p v-if="group.description" class="group-description">{{ group.description }}</p>
|
||||
<GPUTopology
|
||||
v-if="devices.some((device) => group.gpu.deviceIDs.includes(device.id))"
|
||||
:devices="devices.filter((device) => group.gpu.deviceIDs.includes(device.id))"
|
||||
:nodes="
|
||||
numaNodes.filter((node) =>
|
||||
devices.some(
|
||||
(device) =>
|
||||
group.gpu.deviceIDs.includes(device.id) && device.numaNode === node.id,
|
||||
),
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="missing-group-devices">
|
||||
<el-tag
|
||||
v-for="id in group.gpu.deviceIDs.filter(
|
||||
(id) => !devices.some((device) => device.id === id),
|
||||
)"
|
||||
:key="id"
|
||||
type="warning"
|
||||
>
|
||||
{{ id }} · {{ $t('aiTools.model.gpuMissing') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="group-card-meta">
|
||||
<div class="group-binding">
|
||||
<span>{{ $t('aiTools.gpuGroup.affinity') }}</span>
|
||||
<strong>
|
||||
{{
|
||||
$t(
|
||||
group.gpu.numa?.mode === 'auto'
|
||||
? 'aiTools.gpuGroup.followGPU'
|
||||
: group.gpu.numa?.mode === 'manual'
|
||||
? 'aiTools.model.numaManual'
|
||||
: 'aiTools.model.numaOff',
|
||||
)
|
||||
}}
|
||||
</strong>
|
||||
<div v-if="group.gpu.numa && group.gpu.numa.mode !== 'off'">
|
||||
<div v-for="id in groupNodes(group)" :key="id">
|
||||
NUMA {{ id }} · CPU {{ numaNodes.find((node) => node.id === id)?.cpus || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-deployments">
|
||||
<span>{{ $t('aiTools.gpuGroup.deployments') }}</span>
|
||||
<div>{{ group.deployments.join(', ') || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<GPUTopology v-else :devices="numaDevices" :nodes="numaNodes" cards>
|
||||
<template #device-meta="{ device }">
|
||||
<div class="device-memberships">
|
||||
<template v-if="deviceGroups.get(device.id)?.length">
|
||||
<span>{{ $t('aiTools.gpuGroup.groups') }}</span>
|
||||
<el-button
|
||||
v-for="group in deviceGroups.get(device.id)"
|
||||
:key="group.id"
|
||||
v-permission="'ai_gpu_manage'"
|
||||
link
|
||||
type="primary"
|
||||
@click="open(group)"
|
||||
>
|
||||
{{ group.name }}
|
||||
</el-button>
|
||||
</template>
|
||||
<span v-else>{{ $t('aiTools.gpuGroup.ungrouped') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</GPUTopology>
|
||||
<el-alert
|
||||
v-if="!loading && ['issues', 'loadFailed'].includes(environment.status)"
|
||||
class="mb-3"
|
||||
:closable="false"
|
||||
type="warning"
|
||||
:title="$t(`aiTools.gpu.environment.${environment.status}`, { count: environment.issueCount })"
|
||||
/>
|
||||
<el-empty v-if="noDevices" :description="$t('aiTools.gpu.environment.notDetected')" />
|
||||
<GroupTopology
|
||||
v-else-if="!loading && !loadFailed"
|
||||
:key="currentNode"
|
||||
:devices="numaDevices"
|
||||
:nodes="numaNodes"
|
||||
:groups="groups"
|
||||
@edit="open"
|
||||
@remove="remove"
|
||||
@create="open(undefined, $event)"
|
||||
/>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<DrawerPro
|
||||
@@ -131,39 +52,43 @@
|
||||
:disabled="saving"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model.trim="form.name" maxlength="128" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.gpuGroup.vendor')" prop="imageType">
|
||||
<el-select v-model="form.imageType" @change="changeVendor">
|
||||
<el-option
|
||||
v-for="vendor in ['nvidia', 'intel', 'ascend']"
|
||||
:key="vendor"
|
||||
:value="vendor"
|
||||
:label="vendor === 'nvidia' ? 'NVIDIA' : vendor === 'intel' ? 'Intel' : 'Ascend'"
|
||||
/>
|
||||
</el-select>
|
||||
<h3 class="group-form-heading">{{ $t('home.baseInfo') }}</h3>
|
||||
<div class="group-form-basics">
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model.trim="form.name" maxlength="128" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('aiTools.gpuGroup.vendor')" prop="imageType">
|
||||
<el-select v-model="form.imageType" @change="changeVendor">
|
||||
<el-option
|
||||
v-for="vendor in ['nvidia', 'intel', 'ascend']"
|
||||
:key="vendor"
|
||||
:value="vendor"
|
||||
:label="vendor === 'nvidia' ? 'NVIDIA' : vendor === 'intel' ? 'Intel' : 'Ascend'"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
<GPUConfig ref="gpuRef" v-model="form.gpu" :image-type="form.imageType" group-editor>
|
||||
<template #device-meta="{ device }">
|
||||
<span
|
||||
v-if="deviceGroups.get(device.id)?.some((group) => group.id !== form.id)"
|
||||
v-if="
|
||||
groups.some((group) => group.id !== form.id && group.gpu.deviceIDs.includes(device.id))
|
||||
"
|
||||
class="device-existing-groups"
|
||||
>
|
||||
{{ $t('aiTools.gpuGroup.groups') }} ·
|
||||
{{
|
||||
deviceGroups
|
||||
.get(device.id)
|
||||
?.filter((group) => group.id !== form.id)
|
||||
groups
|
||||
.filter((group) => group.id !== form.id && group.gpu.deviceIDs.includes(device.id))
|
||||
.map((group) => group.name)
|
||||
.join(', ')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</GPUConfig>
|
||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" maxlength="500" show-word-limit />
|
||||
</el-form-item>
|
||||
<div v-if="form.id" class="input-help">{{ $t('aiTools.gpuGroup.snapshotHelp') }}</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -180,10 +105,11 @@
|
||||
import { computed, nextTick, onMounted, onActivated, ref, watch } from 'vue';
|
||||
import { ElMessageBox, FormInstance } from 'element-plus';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { deleteGPUGroup, listGPUGroups, loadModelGPUDevices, saveGPUGroup } from '@/api/modules/ai';
|
||||
import { deleteGPUGroup, listGPUGroups, loadGPUInfo, loadModelGPUDevices, saveGPUGroup } from '@/api/modules/ai';
|
||||
import GPUConfig from '@/views/ai/components/gpu-config/index.vue';
|
||||
import GPUTopology from '@/views/ai/components/gpu-topology/index.vue';
|
||||
import GroupTopology from './topology.vue';
|
||||
import RouterMenu from '../index.vue';
|
||||
import { getGPUEnvironmentState } from '../environment';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
@@ -192,29 +118,23 @@ import DrawerPro from '@/components/drawer-pro/index.vue';
|
||||
|
||||
const { currentNode } = useGlobalStore();
|
||||
|
||||
const viewMode = ref<'group' | 'numa'>('group');
|
||||
const groups = ref<AI.GPUGroup[]>([]);
|
||||
const devices = ref<AI.ModelGPUDevice[]>([]);
|
||||
const numaNodes = ref<AI.ModelNUMANode[]>([]);
|
||||
const deviceGroups = computed(() => {
|
||||
const memberships = new Map<string, AI.GPUGroup[]>();
|
||||
for (const group of groups.value) {
|
||||
for (const id of new Set(group.gpu.deviceIDs)) {
|
||||
const entries = memberships.get(id) || [];
|
||||
entries.push(group);
|
||||
memberships.set(id, entries);
|
||||
}
|
||||
}
|
||||
return memberships;
|
||||
});
|
||||
const numaDevices = computed(() => {
|
||||
const result = new Map(devices.value.map((device) => [device.id, device]));
|
||||
for (const [id, memberships] of deviceGroups.value) {
|
||||
if (!result.has(id)) result.set(id, { id, name: id, type: memberships[0].imageType, available: false });
|
||||
for (const group of groups.value) {
|
||||
for (const id of group.gpu.deviceIDs) {
|
||||
if (!result.has(id)) result.set(id, { id, name: id, type: group.imageType, available: false });
|
||||
}
|
||||
}
|
||||
return [...result.values()];
|
||||
});
|
||||
const loading = ref(false);
|
||||
const loadFailed = ref(false);
|
||||
const gpuInfo = ref<AI.Info>();
|
||||
const environment = computed(() => getGPUEnvironmentState(gpuInfo.value, loadFailed.value));
|
||||
const noDevices = computed(() => !loading.value && environment.value.status === 'notDetected' && !groups.value.length);
|
||||
const saving = ref(false);
|
||||
const visible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
@@ -248,27 +168,26 @@ const load = async () => {
|
||||
const version = ++requestVersion;
|
||||
const node = currentNode.value;
|
||||
loading.value = true;
|
||||
loadFailed.value = false;
|
||||
try {
|
||||
const [groupResult, deviceResult] = await Promise.all([listGPUGroups(node), loadModelGPUDevices(node)]);
|
||||
const [groupResult, deviceResult, infoResult] = await Promise.all([
|
||||
listGPUGroups(node),
|
||||
loadModelGPUDevices(node),
|
||||
loadGPUInfo(true, node),
|
||||
]);
|
||||
if (version !== requestVersion || node !== currentNode.value) return;
|
||||
gpuInfo.value = infoResult.data;
|
||||
groups.value = groupResult.data || [];
|
||||
devices.value = deviceResult.data.devices || [];
|
||||
numaNodes.value = deviceResult.data.numaNodes || [];
|
||||
} catch (error) {
|
||||
if (version === requestVersion) loadFailed.value = true;
|
||||
throw error;
|
||||
} finally {
|
||||
if (version === requestVersion) loading.value = false;
|
||||
}
|
||||
};
|
||||
const groupNodes = (group: AI.GPUGroup) =>
|
||||
group.gpu.numa?.mode === 'auto'
|
||||
? [
|
||||
...new Set(
|
||||
devices.value
|
||||
.filter((device) => group.gpu.deviceIDs.includes(device.id) && device.numaNode !== undefined)
|
||||
.map((device) => device.numaNode),
|
||||
),
|
||||
]
|
||||
: group.gpu.numa?.nodes || [];
|
||||
const open = async (group?: AI.GPUGroup) => {
|
||||
const open = async (group?: AI.GPUGroup, deviceIDs: string[] = []) => {
|
||||
form.value = group
|
||||
? {
|
||||
id: group.id,
|
||||
@@ -277,7 +196,16 @@ const open = async (group?: AI.GPUGroup) => {
|
||||
imageType: group.imageType,
|
||||
gpu: JSON.parse(JSON.stringify(group.gpu)),
|
||||
}
|
||||
: { id: 0, name: '', description: '', imageType: 'nvidia', gpu: emptyGPU() };
|
||||
: {
|
||||
id: 0,
|
||||
name: '',
|
||||
description: '',
|
||||
imageType:
|
||||
devices.value.find((device) => deviceIDs.includes(device.id))?.type ||
|
||||
devices.value.find((device) => device.available)?.type ||
|
||||
'nvidia',
|
||||
gpu: { ...emptyGPU(), deviceIDs },
|
||||
};
|
||||
visible.value = true;
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
@@ -320,10 +248,12 @@ const remove = async (group: AI.GPUGroup) => {
|
||||
};
|
||||
onMounted(load);
|
||||
onActivated(() => {
|
||||
if (!loading.value) load();
|
||||
if (!loading.value && !visible.value) load();
|
||||
});
|
||||
watch(currentNode, () => {
|
||||
visible.value = false;
|
||||
form.value = { id: 0, name: '', description: '', imageType: 'nvidia', gpu: emptyGPU() };
|
||||
gpuInfo.value = undefined;
|
||||
groups.value = [];
|
||||
devices.value = [];
|
||||
numaNodes.value = [];
|
||||
@@ -332,13 +262,21 @@ watch(currentNode, () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-description {
|
||||
margin: 0 0 16px;
|
||||
.group-help {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.group-form-heading {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.group-form-basics {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.device-existing-groups {
|
||||
width: 100%;
|
||||
@@ -346,110 +284,8 @@ watch(currentNode, () => {
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.device-memberships {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.device-memberships :deep(.el-button) {
|
||||
margin-left: 0;
|
||||
height: auto;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.device-memberships :deep(.el-button > span) {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.gpu-group-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(440px, 100%), 1fr));
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.gpu-group-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
border-color: var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.gpu-group-card :deep(.el-card__header) {
|
||||
border-bottom: 0;
|
||||
padding: 20px 20px 16px;
|
||||
}
|
||||
.gpu-group-card :deep(.el-card__body) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
.group-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.group-card-header > strong {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.group-card-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.group-card-meta {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
margin-top: auto;
|
||||
padding-top: 24px;
|
||||
}
|
||||
.group-binding,
|
||||
.group-deployments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.group-binding > strong {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
}
|
||||
.group-binding > span,
|
||||
.group-deployments > span {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.missing-group-devices :deep(.el-tag) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.missing-group-devices :deep(.el-tag__content) {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.missing-group-devices:not(:empty) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.missing-group-devices {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.group-card-meta {
|
||||
.group-form-basics {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<el-card shadow="never" class="relation-card">
|
||||
<div ref="board" class="relation-board">
|
||||
<svg class="relation-lines" aria-hidden="true">
|
||||
<path v-for="line in lines" :key="line.key" :d="line.path" :class="{ active: line.active }" />
|
||||
</svg>
|
||||
<div ref="hardware" class="hardware-column">
|
||||
<h3>{{ $t('aiTools.model.gpuDevices') }}</h3>
|
||||
<section
|
||||
v-for="section in sections"
|
||||
:key="section.key"
|
||||
class="numa-section"
|
||||
:class="{
|
||||
unassigned: section.key === 'unassigned',
|
||||
separated: section.key === 'unassigned' && nodes.length > 0,
|
||||
}"
|
||||
>
|
||||
<header v-if="section.label">
|
||||
<div>
|
||||
<strong>{{ section.label }}</strong>
|
||||
<div v-if="section.node" class="input-help">CPU {{ section.node.cpus || '—' }}</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="section.node"
|
||||
v-permission="'ai_gpu_manage'"
|
||||
link
|
||||
type="primary"
|
||||
@click="createFromNode(section.node.id, section.devices)"
|
||||
>
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
</header>
|
||||
<div
|
||||
v-for="device in section.devices"
|
||||
:key="device.id"
|
||||
:data-device="device.id"
|
||||
class="device-node"
|
||||
:class="{ active: activeDevices.has(device.id), unavailable: !device.available }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="node-button"
|
||||
:aria-pressed="focus?.type === 'device' && focus.id === device.id"
|
||||
@click="
|
||||
focus =
|
||||
focus?.type === 'device' && focus.id === device.id
|
||||
? undefined
|
||||
: { type: 'device', id: device.id }
|
||||
"
|
||||
>
|
||||
{{ device.name }}
|
||||
</button>
|
||||
<el-tag v-if="!device.available" size="small" type="warning">
|
||||
{{ $t('aiTools.model.gpuMissing') }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
v-permission="'ai_gpu_manage'"
|
||||
link
|
||||
type="primary"
|
||||
class="device-create"
|
||||
:disabled="!device.available"
|
||||
:aria-label="`${$t('commons.button.create')} · ${device.name}`"
|
||||
@click="$emit('create', [device.id])"
|
||||
>
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
<span class="relation-port" />
|
||||
</div>
|
||||
<span v-if="!section.devices.length" class="input-help">
|
||||
{{ $t('aiTools.gpuGroup.noDevices') }}
|
||||
</span>
|
||||
</section>
|
||||
</div>
|
||||
<div ref="groupColumn" class="groups-column">
|
||||
<h3>{{ $t('commons.table.group') }}</h3>
|
||||
<div class="group-nodes">
|
||||
<el-empty v-if="!groups.length" :image-size="60" />
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
:data-group="group.id"
|
||||
class="relation-group"
|
||||
:class="{ active: activeGroups.has(group.id) }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="node-button"
|
||||
:aria-pressed="focus?.type === 'group' && focus.id === group.id"
|
||||
@click="
|
||||
focus =
|
||||
focus?.type === 'group' && focus.id === group.id
|
||||
? undefined
|
||||
: { type: 'group', id: group.id }
|
||||
"
|
||||
>
|
||||
<strong>{{ group.name || $t('commons.button.create') }}</strong>
|
||||
<span class="input-help">{{ group.gpu.deviceIDs.length }} GPU · {{ group.imageType }}</span>
|
||||
</button>
|
||||
<span class="relation-port" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="focusedGroup" class="relation-detail">
|
||||
<div class="detail-heading">
|
||||
<div class="detail-title">
|
||||
<strong>{{ focusedGroup.name }}</strong>
|
||||
<el-tag size="small" type="info" effect="plain">{{ focusedGroup.gpu.deviceIDs.length }} GPU</el-tag>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<el-button v-permission="'ai_gpu_manage'" link type="primary" @click="$emit('edit', focusedGroup)">
|
||||
{{ $t('commons.button.edit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-permission="'ai_gpu_manage'"
|
||||
link
|
||||
type="primary"
|
||||
@click="$emit('remove', focusedGroup)"
|
||||
>
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="focusedGroup.description" class="detail-description">{{ focusedGroup.description }}</p>
|
||||
<dl class="detail-fields">
|
||||
<div>
|
||||
<dt>{{ $t('aiTools.gpuGroup.affinity') }}</dt>
|
||||
<dd>
|
||||
<span>
|
||||
{{
|
||||
$t(
|
||||
focusedGroup.gpu.numa?.mode === 'auto'
|
||||
? 'aiTools.gpuGroup.followGPU'
|
||||
: focusedGroup.gpu.numa?.mode === 'manual'
|
||||
? 'aiTools.model.numaManual'
|
||||
: 'aiTools.model.numaOff',
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<div v-if="focusedGroup.gpu.numa?.mode === 'manual'" class="detail-tags">
|
||||
<el-tag
|
||||
v-for="node in focusedGroup.gpu.numa.nodes"
|
||||
:key="node"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>
|
||||
NUMA {{ node }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ $t('aiTools.gpuGroup.deployments') }}</dt>
|
||||
<dd class="detail-tags">
|
||||
<el-tag
|
||||
v-for="deployment in focusedGroup.deployments"
|
||||
:key="deployment"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>
|
||||
{{ deployment }}
|
||||
</el-tag>
|
||||
<span v-if="!focusedGroup.deployments.length" class="detail-empty">—</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-else-if="focusedDevice" class="relation-detail">
|
||||
<div class="detail-heading">
|
||||
<div class="detail-title">
|
||||
<strong>{{ focusedDevice.name }}</strong>
|
||||
<el-tag v-if="!focusedDevice.available" size="small" type="warning">
|
||||
{{ $t('aiTools.model.gpuMissing') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button
|
||||
v-permission="'ai_gpu_manage'"
|
||||
link
|
||||
type="primary"
|
||||
:disabled="!focusedDevice.available"
|
||||
@click="$emit('create', [focusedDevice.id])"
|
||||
>
|
||||
{{ $t('commons.button.create') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<dl class="detail-fields device-fields">
|
||||
<div>
|
||||
<dt>{{ $t('commons.table.group') }}</dt>
|
||||
<dd class="detail-tags">
|
||||
<el-tag
|
||||
v-for="group in groups.filter((group) => group.gpu.deviceIDs.includes(focusedDevice.id))"
|
||||
:key="group.id"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>
|
||||
{{ group.name }}
|
||||
</el-tag>
|
||||
<span
|
||||
v-if="!groups.some((group) => group.gpu.deviceIDs.includes(focusedDevice.id))"
|
||||
class="detail-empty"
|
||||
>
|
||||
{{ $t('aiTools.gpuGroup.ungrouped') }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, nextTick, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const props = defineProps<{
|
||||
devices: AI.ModelGPUDevice[];
|
||||
nodes: AI.ModelNUMANode[];
|
||||
groups: AI.GPUGroup[];
|
||||
}>();
|
||||
const emit = defineEmits<{ create: [ids: string[]]; edit: [group: AI.GPUGroup]; remove: [group: AI.GPUGroup] }>();
|
||||
const focus = ref<{ type: 'device'; id: string } | { type: 'group'; id: number }>();
|
||||
const board = ref<HTMLElement>();
|
||||
const hardware = ref<HTMLElement>();
|
||||
const groupColumn = ref<HTMLElement>();
|
||||
const lines = ref<{ key: string; path: string; active: boolean }[]>([]);
|
||||
const sections = computed(() => {
|
||||
const available = props.devices.filter((device) => device.available);
|
||||
const result: { key: string; label: string; node?: AI.ModelNUMANode; devices: AI.ModelGPUDevice[] }[] =
|
||||
props.nodes.map((node) => ({
|
||||
key: `numa-${node.id}`,
|
||||
label: `NUMA ${node.id}`,
|
||||
node,
|
||||
devices: available.filter((device) => device.numaNode === node.id),
|
||||
}));
|
||||
const unassigned = available.filter((device) => !props.nodes.some((node) => node.id === device.numaNode));
|
||||
const missing = props.devices.filter((device) => !device.available);
|
||||
if (missing.length)
|
||||
result.push({
|
||||
key: 'missing',
|
||||
label: i18n.global.t('aiTools.model.gpuMissing'),
|
||||
node: undefined,
|
||||
devices: missing,
|
||||
});
|
||||
if (unassigned.length) result.push({ key: 'unassigned', label: '', devices: unassigned });
|
||||
return result;
|
||||
});
|
||||
const activeGroups = computed(
|
||||
() =>
|
||||
new Set(
|
||||
props.groups
|
||||
.filter((group) =>
|
||||
focus.value?.type === 'group'
|
||||
? group.id === focus.value.id
|
||||
: focus.value?.type === 'device' && group.gpu.deviceIDs.includes(focus.value.id),
|
||||
)
|
||||
.map((group) => group.id),
|
||||
),
|
||||
);
|
||||
const activeDevices = computed(
|
||||
() =>
|
||||
new Set(
|
||||
focus.value?.type === 'device'
|
||||
? [focus.value.id]
|
||||
: props.groups
|
||||
.filter((group) => activeGroups.value.has(group.id))
|
||||
.flatMap((group) => group.gpu.deviceIDs),
|
||||
),
|
||||
);
|
||||
const focusedGroup = computed(() =>
|
||||
props.groups.find((group) => focus.value?.type === 'group' && group.id === focus.value.id),
|
||||
);
|
||||
const focusedDevice = computed(() =>
|
||||
props.devices.find((device) => focus.value?.type === 'device' && device.id === focus.value.id),
|
||||
);
|
||||
const createFromNode = async (node: number, devices: AI.ModelGPUDevice[]) => {
|
||||
const available = devices.filter((device) => device.available);
|
||||
if (!available.length) {
|
||||
await ElMessageBox.alert(
|
||||
h('div', [
|
||||
h('p', i18n.global.t('aiTools.gpuGroup.emptyNodeCreate', { node })),
|
||||
h('p', { class: 'mt-3' }, i18n.global.t('aiTools.gpuGroup.inspectNUMA')),
|
||||
h(
|
||||
'pre',
|
||||
{ class: 'my-3', style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } },
|
||||
'lspci -Dnn\ngrep -H . /sys/bus/pci/devices/*/numa_node',
|
||||
),
|
||||
h('p', i18n.global.t('aiTools.gpuGroup.numaQueryHint')),
|
||||
]),
|
||||
i18n.global.t('commons.msg.infoTitle'),
|
||||
{
|
||||
type: 'info',
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
customStyle: { width: '640px', maxWidth: 'calc(100vw - 32px)' },
|
||||
},
|
||||
).catch(() => {});
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
'create',
|
||||
available.filter((device) => device.type === available[0].type).map((device) => device.id),
|
||||
);
|
||||
};
|
||||
const drawLines = () => {
|
||||
if (!board.value) return;
|
||||
const rect = board.value.getBoundingClientRect();
|
||||
const devices = new Map(
|
||||
[...board.value.querySelectorAll<HTMLElement>('[data-device]')].map((element) => [
|
||||
element.dataset.device,
|
||||
element.getBoundingClientRect(),
|
||||
]),
|
||||
);
|
||||
const groups = new Map(
|
||||
[...board.value.querySelectorAll<HTMLElement>('[data-group]')].map((element) => [
|
||||
Number(element.dataset.group),
|
||||
element.getBoundingClientRect(),
|
||||
]),
|
||||
);
|
||||
const next: typeof lines.value = [];
|
||||
for (const group of props.groups) {
|
||||
const target = groups.get(group.id);
|
||||
if (!target) continue;
|
||||
for (const id of group.gpu.deviceIDs) {
|
||||
const source = devices.get(id);
|
||||
if (!source) continue;
|
||||
const x1 = source.right - rect.left;
|
||||
const y1 = source.top + source.height / 2 - rect.top;
|
||||
const x2 = target.left - rect.left;
|
||||
const y2 = target.top + target.height / 2 - rect.top;
|
||||
const mid = (x1 + x2) / 2;
|
||||
const active = activeGroups.value.has(group.id) && activeDevices.value.has(id);
|
||||
next.push({
|
||||
key: `${group.id}:${id}`,
|
||||
path: `M ${x1} ${y1} C ${mid} ${y1}, ${mid} ${y2}, ${x2} ${y2}`,
|
||||
active,
|
||||
});
|
||||
}
|
||||
}
|
||||
lines.value = next.sort((a, b) => Number(a.active) - Number(b.active));
|
||||
};
|
||||
useResizeObserver([board, hardware, groupColumn], drawLines);
|
||||
watch(
|
||||
[() => props.groups, sections, activeDevices, activeGroups],
|
||||
async () => {
|
||||
await nextTick();
|
||||
drawLines();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-heading,
|
||||
.numa-section header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.relation-card {
|
||||
border-color: var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.relation-board::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 16px;
|
||||
width: 1px;
|
||||
background: var(--el-border-color-extra-light);
|
||||
pointer-events: none;
|
||||
}
|
||||
.relation-board {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(48px, 12%) minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
.hardware-column {
|
||||
grid-column: 1;
|
||||
}
|
||||
.groups-column {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.group-nodes {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
gap: 20px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.hardware-column,
|
||||
.groups-column {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.groups-column h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.numa-section {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.numa-section header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.numa-section.unassigned {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.numa-section.unassigned.separated {
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 0;
|
||||
padding-top: 16px;
|
||||
}
|
||||
.device-node,
|
||||
.relation-group {
|
||||
position: relative;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.device-node {
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.device-node > .node-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
.device-create {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
.device-node.active,
|
||||
.relation-group.active {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
.device-node.unavailable {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.relation-group {
|
||||
padding: 16px;
|
||||
}
|
||||
.node-button {
|
||||
position: relative;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-height: 24px;
|
||||
}
|
||||
.relation-port {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: calc(50% - 4px);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.relation-group .relation-port {
|
||||
left: -4px;
|
||||
right: auto;
|
||||
}
|
||||
.active > .relation-port {
|
||||
background: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.relation-lines {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.relation-lines path {
|
||||
fill: none;
|
||||
stroke: var(--el-border-color);
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
.relation-lines path.active {
|
||||
stroke: var(--el-color-primary);
|
||||
stroke-width: 2;
|
||||
}
|
||||
.relation-detail {
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.detail-title > strong {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.detail-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
.detail-description {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.detail-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px 32px;
|
||||
margin: 0;
|
||||
}
|
||||
.detail-fields > div {
|
||||
min-width: 0;
|
||||
}
|
||||
.detail-fields dt {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.detail-fields dd {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.detail-fields.device-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.detail-tags,
|
||||
.detail-fields dd.detail-tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.detail-tags :deep(.el-tag) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
min-height: 22px;
|
||||
}
|
||||
.detail-tags :deep(.el-tag__content) {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.detail-empty {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.device-node > .node-button {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.detail-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.relation-card :deep(.el-card__body) {
|
||||
padding: 12px;
|
||||
}
|
||||
.numa-section {
|
||||
padding: 8px;
|
||||
}
|
||||
.device-node,
|
||||
.relation-group {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
.relation-board {
|
||||
grid-template-columns: minmax(0, 1fr) 32px minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -86,7 +86,14 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-empty v-else-if="!loading" :description="$t('aiTools.gpu.environment.notDetected')" />
|
||||
<el-empty
|
||||
v-else-if="!loading"
|
||||
:description="
|
||||
environment.status === 'passed'
|
||||
? $t('commons.msg.noneData')
|
||||
: $t(`aiTools.gpu.environment.${environment.status}`, { count: environment.issueCount })
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
@@ -96,8 +103,9 @@
|
||||
import { ref, computed, watch, onBeforeUnmount, h } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { loadGPUMonitor, getGPUOptions } from '@/api/modules/ai';
|
||||
import { loadGPUMonitor, getGPUOptions, loadGPUInfo } from '@/api/modules/ai';
|
||||
import RouterMenu from '@/views/ai/gpu/index.vue';
|
||||
import { getGPUEnvironmentState } from '../environment';
|
||||
import { shortcuts } from '@/utils/shortcuts';
|
||||
import i18n from '@/lang';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
@@ -108,6 +116,8 @@ const { isMobile } = useGlobalStore();
|
||||
const loading = ref(false);
|
||||
const failed = ref(false);
|
||||
const supported = ref(false);
|
||||
const gpuInfo = ref<AI.Info>();
|
||||
const environment = computed(() => getGPUEnvironmentState(gpuInfo.value, failed.value));
|
||||
const options = ref<(AI.ChartHide & { value: string; label: string })[]>([]);
|
||||
const selectedDevice = ref('');
|
||||
const aggregation = ref<'avg' | 'max'>('avg');
|
||||
@@ -362,6 +372,11 @@ const loadOptions = async () => {
|
||||
.replace(/^(\d+) - /, `${item.type === 'xpu' ? 'XPU' : 'GPU'} $1 · `)
|
||||
.replace(' - ', ' · '),
|
||||
}));
|
||||
if (!options.value.length) {
|
||||
const infoResult = await loadGPUInfo(true);
|
||||
if (id !== requestID) return;
|
||||
gpuInfo.value = infoResult.data;
|
||||
}
|
||||
if (!currentDevice.value && !route.query.deviceID) selectedDevice.value = options.value[0]?.value || '';
|
||||
await search();
|
||||
} catch {
|
||||
|
||||
@@ -20,14 +20,14 @@ const props = defineProps({
|
||||
|
||||
const buttons = computed(() => [
|
||||
{
|
||||
label: i18n.global.t('aiTools.gpuGroup.overview'),
|
||||
label: i18n.global.t('aiTools.gpu.current'),
|
||||
path: '/ai/gpu/current',
|
||||
},
|
||||
{ label: i18n.global.t('aiTools.gpuGroup.groups'), path: '/ai/gpu/groups' },
|
||||
{
|
||||
label: i18n.global.t('aiTools.gpuGroup.history'),
|
||||
label: i18n.global.t('aiTools.gpu.history'),
|
||||
path: '/ai/gpu/history',
|
||||
disabled: props.historyDisabled,
|
||||
},
|
||||
{ label: i18n.global.t('website.groupSetting'), path: '/ai/gpu/groups' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user