Skip to content

Commit eec57a2

Browse files
authored
Merge pull request #323 from HIRO-MicroDataCenters-BV/COG-389/llm-recomendation
Cog 389/llm recomendation
2 parents 715d6e6 + 08390f7 commit eec57a2

5 files changed

Lines changed: 565 additions & 21 deletions

File tree

components/app/dialog/ServeModelDialog.vue

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,36 @@
222222
size="sm"
223223
class="h-9 px-3 text-xs cursor-pointer"
224224
:disabled="
225+
!llm.hf_model_id.trim() ||
225226
typeof llm.concurrent_users !== 'number' ||
226-
llm.concurrent_users < 1
227+
llm.concurrent_users < 1 ||
228+
isAutofilling
227229
"
228230
@click="autofillModelSettings"
229231
>
230-
<Icon name="lucide:wand-2" class="h-3 w-3 mr-1" />
232+
<Icon
233+
:name="isAutofilling ? 'lucide:loader-2' : 'lucide:wand-2'"
234+
:class="[
235+
'h-3 w-3 mr-1',
236+
isAutofilling ? 'animate-spin' : '',
237+
]"
238+
/>
231239
Autofill
232240
</Button>
233241
</div>
234-
<p class="text-[11px] text-muted-foreground mt-1">
235-
Enter expected concurrent users and click Autofill to suggest
236-
model settings.
242+
<p
243+
v-if="autofillError"
244+
class="text-[11px] text-destructive flex items-start gap-1 mt-1"
245+
>
246+
<Icon
247+
name="lucide:alert-circle"
248+
class="w-3 h-3 mt-0.5 shrink-0"
249+
/>
250+
<span>{{ autofillError }}</span>
251+
</p>
252+
<p v-else class="text-[11px] text-muted-foreground mt-1">
253+
Enter HF Model ID and expected concurrent users, then click
254+
Autofill to fetch recommended model settings.
237255
</p>
238256
</FieldRow>
239257
<FieldRow label="HF token">
@@ -439,10 +457,13 @@ const emit = defineEmits<{
439457
(e: 'created'): void;
440458
}>();
441459
442-
const { postModelServing } = useApi();
460+
const { postModelServing, recommendModelServing } = useApi();
461+
const toaster = useToaster();
443462
444463
const mode = ref<Mode>('classical');
445464
const isSubmitting = ref(false);
465+
const isAutofilling = ref(false);
466+
const autofillError = ref<string | null>(null);
446467
447468
const blankClassical = () => ({
448469
isvc_name: '',
@@ -600,26 +621,88 @@ const recommendForUsers = (users: number): RecommendedSettings => {
600621
};
601622
};
602623
603-
const autofillModelSettings = () => {
624+
const autofillModelSettings = async () => {
604625
const users = llm.concurrent_users;
605-
if (typeof users !== 'number' || users < 1) return;
606-
const r = recommendForUsers(users);
607-
llm.dtype = r.dtype;
608-
llm.max_model_len = r.max_model_len;
609-
llm.tensor_parallel_size = r.tensor_parallel_size;
610-
llm.res_cpu_req = r.res_cpu_req;
611-
llm.res_cpu_lim = r.res_cpu_lim;
612-
llm.res_mem_req = r.res_mem_req;
613-
llm.res_mem_lim = r.res_mem_lim;
614-
llm.res_gpu_req = r.res_gpu_req;
615-
llm.res_gpu_lim = r.res_gpu_lim;
626+
const hfModelId = llm.hf_model_id.trim();
627+
if (!hfModelId || typeof users !== 'number' || users < 1) return;
628+
629+
autofillError.value = null;
630+
isAutofilling.value = true;
631+
try {
632+
type RecommendationItem = {
633+
profile?: string;
634+
max_model_len?: number;
635+
dtype?: string;
636+
tensor_parallel_size?: number;
637+
min_replicas?: number;
638+
max_replicas?: number;
639+
resources?: {
640+
requests?: Record<string, string>;
641+
limits?: Record<string, string>;
642+
};
643+
};
644+
type RecommendData = { recommendations?: RecommendationItem[] };
645+
646+
const res = (await recommendModelServing(
647+
{
648+
hf_model_id: hfModelId,
649+
concurrent_users: users,
650+
expected_input_tokens: 2048,
651+
expected_output_tokens: 1024,
652+
quantization: 'none',
653+
profiles: ['throughput'],
654+
},
655+
{
656+
onConflict: (msg) => {
657+
autofillError.value = msg;
658+
},
659+
},
660+
)) as { data?: RecommendData } | null;
661+
662+
if (autofillError.value) return;
663+
664+
const local = recommendForUsers(users);
665+
const recs = Array.isArray(res?.data?.recommendations)
666+
? res.data.recommendations
667+
: [];
668+
const rec: RecommendationItem | undefined =
669+
recs.find((r) => r?.profile === 'throughput') ?? recs[0];
670+
671+
const pickStr = (v: unknown): string | undefined =>
672+
typeof v === 'string' && v.length > 0 ? v : undefined;
673+
const pickNum = (v: unknown): number | undefined =>
674+
typeof v === 'number' && Number.isFinite(v) ? v : undefined;
675+
676+
const requests = rec?.resources?.requests ?? {};
677+
const limits = rec?.resources?.limits ?? {};
678+
679+
llm.dtype = pickStr(rec?.dtype) ?? local.dtype;
680+
llm.max_model_len = pickNum(rec?.max_model_len) ?? local.max_model_len;
681+
llm.tensor_parallel_size =
682+
pickNum(rec?.tensor_parallel_size) ?? local.tensor_parallel_size;
683+
llm.res_cpu_req = pickStr(requests.cpu) ?? local.res_cpu_req;
684+
llm.res_cpu_lim = pickStr(limits.cpu) ?? local.res_cpu_lim;
685+
llm.res_mem_req = pickStr(requests.memory) ?? local.res_mem_req;
686+
llm.res_mem_lim = pickStr(limits.memory) ?? local.res_mem_lim;
687+
llm.res_gpu_req = pickStr(requests['nvidia.com/gpu']) ?? local.res_gpu_req;
688+
llm.res_gpu_lim = pickStr(limits['nvidia.com/gpu']) ?? local.res_gpu_lim;
689+
llm.min_replicas = pickNum(rec?.min_replicas) ?? llm.min_replicas;
690+
llm.max_replicas = pickNum(rec?.max_replicas) ?? llm.max_replicas;
691+
692+
if (rec) {
693+
toaster.show('success', 'operation_completed');
694+
}
695+
} finally {
696+
isAutofilling.value = false;
697+
}
616698
};
617699
618700
const resetState = () => {
619701
Object.assign(classical, blankClassical());
620702
Object.assign(llm, blankLlm());
621703
mode.value = 'classical';
622704
isSubmitting.value = false;
705+
autofillError.value = null;
623706
};
624707
625708
watch(

composables/api.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const useApi = () => {
151151
showToast?: boolean;
152152
successMessage?: string;
153153
useResponseMessage?: boolean;
154+
onConflict?: (message: string) => void;
154155
},
155156
) => {
156157
const isFormData = body instanceof FormData;
@@ -226,6 +227,30 @@ export const useApi = () => {
226227
toaster.show('error', 'not_found');
227228
}
228229
return null;
230+
case 409: {
231+
// Handle conflict errors - detail may be an object with a message field
232+
// (e.g. /models-serving/recommend returns rich scheduling info on 409).
233+
const conflictDetail = data?.detail;
234+
const conflictMsg =
235+
conflictDetail &&
236+
typeof conflictDetail === 'object' &&
237+
typeof (conflictDetail as { message?: unknown }).message ===
238+
'string'
239+
? (conflictDetail as { message: string }).message
240+
: typeof conflictDetail === 'string'
241+
? conflictDetail
242+
: typeof data?.message === 'string'
243+
? data.message
244+
: null;
245+
if (options?.onConflict && conflictMsg) {
246+
options.onConflict(conflictMsg);
247+
} else if (conflictMsg) {
248+
toaster.show('error', conflictMsg, undefined, { raw: true });
249+
} else {
250+
toaster.show('error', 'request_failed');
251+
}
252+
return null;
253+
}
229254
case 422: {
230255
// Handle validation errors - detail might be an array or object
231256
const validationMsg =
@@ -436,6 +461,32 @@ export const useApi = () => {
436461
);
437462
},
438463

464+
/**
465+
* Requests recommended LLM serving settings for the given workload.
466+
*
467+
* @param {Object} data - Workload description used by the recommender
468+
* @returns {Promise<Object>} Response containing recommended model settings
469+
*/
470+
recommendModelServing: async (
471+
data: {
472+
hf_model_id: string;
473+
concurrent_users: number;
474+
expected_input_tokens?: number;
475+
expected_output_tokens?: number;
476+
quantization?: string;
477+
profiles?: string[];
478+
},
479+
options?: {
480+
showToast?: boolean;
481+
onConflict?: (message: string) => void;
482+
},
483+
) => {
484+
return request(`/models-serving/recommend`, 'POST', data, {
485+
showToast: options?.showToast ?? false,
486+
onConflict: options?.onConflict,
487+
});
488+
},
489+
439490
/**
440491
* Retrieves all served models
441492
*

composables/mock.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,36 @@ export const useApiWithMock = () => {
815815
});
816816
},
817817

818+
recommendModelServing: async (
819+
data: {
820+
hf_model_id: string;
821+
concurrent_users: number;
822+
expected_input_tokens?: number;
823+
expected_output_tokens?: number;
824+
quantization?: string;
825+
profiles?: string[];
826+
},
827+
_options?: {
828+
showToast?: boolean;
829+
onConflict?: (message: string) => void;
830+
},
831+
) => {
832+
if (mock.value.enabled) {
833+
await mockDelay();
834+
const recommendJson = await import(
835+
'~/mocks/post.models-serving.recommend.json'
836+
);
837+
return Promise.resolve(
838+
recommendJson as unknown as {
839+
status_code: number;
840+
message: string;
841+
data: unknown;
842+
},
843+
);
844+
}
845+
return request(`/models-serving/recommend`, 'POST', data);
846+
},
847+
818848
patchModelServing: async (
819849
data: {
820850
isvc_name: string;

0 commit comments

Comments
 (0)