You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<back-header> 显卡管理 > {{ detail.uuid }} </back-header>
<block-box class="node-block">
<div class="card-detail">
<div class="card-detail-left">
<div class="title">详细信息</div>
<ul class="card-detail-info">
<li v-for="{ label, value, render } in columns" :key="label">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
</ul>
</div>
</div>
</block-box>
<block-box>
<ul class="card-gauges">
<li v-for="(item, index) in gaugeConfig" :key="index">
<template v-if="!detail.isExternal || index >= 2">
<Gauge v-bind="item" />
</template>
<template v-else-if="detail.isExternal && index < 2">
<el-empty description="暂无资源分配数据" :image-size="90" />
</template>
</li>
<li v-for="(item, index) in lineTools" :key="index">
<Gauge v-bind="item" />
</li>
</ul>
</block-box>
<div class="line-box">
<block-box title="资源分配趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus
:options="
getRangeOptions({
core: gaugeConfig[0].data,
memory: gaugeConfig[1].data,
})
"
/>
</div>
</block-box>
<block-box title="资源使用趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus
:options="
getRangeOptions({
core: gaugeConfig[2].data,
memory: gaugeConfig[3].data,
})
"
/>
</div>
</block-box>
<block-box :title="title" v-for="{ title, data, unit } in lineTools" :key="title">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus :options="getLineOptions2({ data, unit })" />
</div>
</block-box>
</div>
<block-box title="任务列表">
<template v-if="detail.isExternal">
<el-alert title="由于显卡未纳管,无法获取到任务数据" show-icon type="warning" :closable="false" />
<el-empty description="暂无任务数据" :image-size="100" />
</template>
<template v-else>
<TaskList :hideTitle="true" :filters="{ deviceId: detail.uuid }" />
</template>
</block-box>
</div>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import { useRoute } from 'vue-router';
import BlockBox from '@/components/BlockBox.vue';
import { onMounted, ref, watch, defineProps } from 'vue';
import TaskList from '~/vgpu/views/task/admin/index.vue';
import {ElPopover} from 'element-plus';
import Gauge from '~/vgpu/components/gauge.vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import { QuestionFilled } from '@element-plus/icons-vue';
import EchartsPlus from '@/components/Echarts-plus.vue';
import cardApi from '~/vgpu/api/card';
import { timeParse } from '@/utils';
import { getLineOptions } from '~/vgpu/views/monitor/overview/getOptions';
import { getLineOptions as getLineOptions2 } from '~/vgpu/components/config';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
import { getRangeOptions } from './getOptions';
const props = defineProps([
'title',
'detailColumns',
'type',
'detail',
'name',
'filters',
'hideCp',
]);
const route = useRoute();
const detail = ref({});
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const columns = [
{
label: '显卡状态',
value: 'health',
render: ({ health, isExternal }) => {
if (detail.value && detail.value.health !== undefined) {
const text = health ? '健康' : '硬件错误';
const color = health ? '#2563eb' : '#EF4444';
return (
<div
style={{
color,
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '5px',
}}
>
<el-tag disable-transitions type={isExternal ? 'warning' : (health ? 'success' : 'danger')}>
{isExternal ? '未纳管' : (health ? '健康' : '硬件错误')}
</el-tag>
{!health && (
<ElPopover trigger="hover" popper-style={{ width: '190px' }}>
{{
reference: () => (
<el-icon color="#939EA9" size="14">
<QuestionFilled />
</el-icon>
),
default: () => (
<span style={{ marginLeft: '5px' }}>
请排查该 GPU 硬件问题
</span>
),
}}
</ElPopover>
)}
</div>
);
} else {
return <el-tag disable-transitions size="small" type="info">加载中...</el-tag>;
}
},
},
// {
// label: '显卡厂商',
// value: 'uuid',
// render: ({ type }) => <span>{type?.split('-')[0]}</span>,
// },
{
label: '显卡 ID',
value: 'uuid',
render: ({ uuid }) => <text-plus text={uuid} copy />,
},
{
label: '所属节点',
value: 'nodeName',
},
{
label: '显卡型号',
value: 'type',
},
{
label: '设备号',
value: 'device_no',
},
{
label: '驱动版本',
value: 'driver_version',
},
{
label: '使用模式',
width: 120,
value: 'mode',
render: ({ mode, type }) => (
<el-tag disable-transitions>
{type?.split('-')[0] === "NVIDIA" ? mode : 'default'}
</el-tag>
)
}
];
// const cp = useInstantVector(
// [
// {
// label: 'vGPU超配',
// count: '0',
// query: `avg(sum(hami_vgpu_count{node=~"$node"}) by (instance))`,
// },
// {
// label: '算力超配',
// count: '0',
// query: `avg(sum(hami_vcore_scaling{node=~"$node"}) by (instance))`,
// },
// {
// label: '显存超配',
// count: '1.5',
// query: `avg(sum(hami_vmemory_scaling{node=~"$node"}) by (instance))`,
// },
// ],
// (query) => query.replaceAll('$node', props.detail.name),
// );
const gaugeConfig = useInstantVector(
[
{
title: '算力分配率',
percent: 0,
query: `avg(sum(hami_container_vcore_allocated{deviceuuid=~"$deviceuuid"}) by (instance))`,
totalQuery: `avg(sum(hami_core_size{deviceuuid=~"$deviceuuid"}) by (instance))`,
percentQuery: `avg(sum(hami_container_vcore_allocated{deviceuuid=~"$deviceuuid"}) by (instance))/avg(sum(hami_core_size{deviceuuid=~"$deviceuuid"}) by (instance)) *100`,
total: 0,
used: 0,
unit: ' ',
},
{
title: '显存分配率',
percent: 0,
query: `avg(sum(hami_container_vmemory_allocated{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024`,
percentQuery: `(avg(sum(hami_container_vmemory_allocated{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024 )/(avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024) *100 `,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_core_util{deviceuuid=~"$deviceuuid"}) by (instance))`,
percentQuery: `avg(sum(hami_core_util_avg{deviceuuid=~"$deviceuuid"}) by (instance))`,
total: 100,
used: 0,
unit: ' ',
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_memory_used{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance))/1024`,
percentQuery: `(avg(sum(hami_memory_used{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024)/(avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance))/1024)*100`,
total: 0,
used: 0,
unit: 'GiB',
},
],
(query) => query.replaceAll(`$deviceuuid`, route.params.uuid),
times,
);
const lineTools = ref([
{
title: 'GPU功率 (W)',
query: `avg by (device_no,driver_version) (hami_device_power{deviceuuid=~"$deviceuuid"})`,
data: [],
unit: 'W',
gaugeUnit: 'W',
percent: 0,
total: 0,
hideInfo: true,
},
{
title: 'GPU 温度(℃)',
query: `avg by (device_no,driver_version) (hami_device_temperature{deviceuuid=~"$deviceuuid"})`,
data: [],
unit: '℃',
gaugeUnit: '℃',
percent: 0,
total: 0,
hideInfo: true,
},
]);
const fetchLineData = async () => {
lineTools.value.map((item, index) => {
cardApi
.getRangeVector({
range: {
start: timeParse(times.value[0]),
end: timeParse(times.value[1]),
step: '1m',
},
query: item.query.replaceAll(`$deviceuuid`, route.params.uuid),
})
.then((res) => {
const { device_no, driver_version } = res.data[0].metric;
if (device_no && driver_version) {
detail.value = { ...detail.value, device_no, driver_version };
}
lineTools.value[index].data = res.data[0]?.values || [];
});
cardApi
.getInstantVector({
query: item.query.replaceAll(`$deviceuuid`, route.params.uuid),
})
.then((res) => {
lineTools.value[index].percent = res.data[0]?.value || 0;
});
});
};
onMounted(async () => {
const res = await cardApi.getCardDetail({ uid: route.params.uuid });
detail.value = { ...detail.value, ...res };
});
watch(
times,
() => {
fetchLineData();
},
{ immediate: true },
);
</script>
<style lang="scss">
.card-detail {
display: flex;
height: 100%;
gap: 50px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
.card-detail-left {
min-width: 1050px;
}
.card-detail-info {
//display: flex;
//flex-direction: column;
gap: 15px;
font-size: 12px;
display: grid;
grid-template-columns: 3fr 2fr;
.label {
display: inline-block;
width: 80px;
height: 20px;
color: #939ea9;
}
.cp {
display: flex;
gap: 25px;
}
}
}
.card-gauges {
margin: 0;
padding: 0;
list-style: none;
display: flex;
height: 200px;
li {
flex: 1;
}
}
.line-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 20px;
}
.node-block {
display: flex;
flex-direction: column;
.home-block-content {
flex: 1;
}
}
</style>