diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 37a57e4c39..dac8d380c3 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -249,17 +249,42 @@ class LlamaCppBackend: ) # Determine the filename from the variant (e.g., "Q4_K_M" -> find matching file) + # For split GGUFs (e.g., *-00001-of-00003.gguf) we must download ALL shards. gguf_filename = None + gguf_extra_shards: list[str] = [] if hf_variant: # Try common naming patterns try: + import re from huggingface_hub import list_repo_files files = list_repo_files(hf_repo, token=hf_token) variant_lower = hf_variant.lower() - for f in files: - if f.endswith(".gguf") and variant_lower in f.lower(): - gguf_filename = f - break + # Use word-boundary matching so "Q8_0" doesn't also + # match "IQ8_0" or other superset variant names. + boundary = re.compile( + r'(?= 0 + and dataset_slice_end >= _slice_start): + # Manual slice — stream only the rows we need instead of + # downloading the entire dataset. + rows_to_stream = dataset_slice_end + 1 + print( + f"[dataset-slice] Manual slice specified " + f"(start={dataset_slice_start}, end={dataset_slice_end}), " + f"streaming {rows_to_stream} rows\n" + ) + stream = load_dataset(**load_kwargs, streaming=True) + dataset = Dataset.from_list(list(stream.take(rows_to_stream))) + print( + f"[dataset-slice] Downloaded {len(dataset)} rows " + f"(requested {rows_to_stream})\n" + ) + self._update_progress( + status_message=f"Streamed {len(dataset)} rows from HuggingFace" + ) + else: + dataset = load_dataset(**load_kwargs) # Check if stopped during dataset loading if self.should_stop: @@ -1877,7 +1901,7 @@ class UnslothTrainer: return None self._update_progress(status_message=f"Loaded dataset from HuggingFace: {dataset_source}") - print(f"Loaded dataset from Hugging Face: {dataset_source}\n") + print(f"Loaded dataset from Hugging Face: {dataset_source} ({len(dataset)} rows)\n") # Resolve eval split from a separate HF split (explicit or auto-detected) if eval_enabled: diff --git a/studio/frontend/src/components/ui/tooltip.tsx b/studio/frontend/src/components/ui/tooltip.tsx index 94fba54cc2..30eb1e7b80 100644 --- a/studio/frontend/src/components/ui/tooltip.tsx +++ b/studio/frontend/src/components/ui/tooltip.tsx @@ -1,6 +1,6 @@ -// SPDX-License-Identifier: AGPL-3.0-only - See /studio/LICENSE.AGPL-3.0 -// Copyright © 2025 Unsloth AI - +// SPDX-License-Identifier: AGPL-3.0-only - See /studio/LICENSE.AGPL-3.0 +// Copyright © 2025 Unsloth AI + import { Tooltip as TooltipPrimitive } from "radix-ui"; import type * as React from "react"; @@ -46,14 +46,14 @@ function TooltipContent({ {children} - + ); diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index c61565cbd9..4f20133b5c 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -89,6 +89,13 @@ function formatUpdatedDate(timestamp: number | null): string { return new Date(timestamp * 1000).toLocaleDateString(); } +function normalizeSliceInput(value: string): string | null { + const trimmed = value.trim(); + if (!trimmed) return null; + if (!/^\d+$/.test(trimmed)) return null; + return trimmed; +} + export function DatasetSection() { const { dataset, @@ -641,6 +648,19 @@ export function DatasetSection() { datasetEvalSplit={datasetEvalSplit} setDatasetEvalSplit={setDatasetEvalSplit} /> + ) : !selectedDatasetName ? ( + ) : datasetSource === "upload" && selectedLocalDataset ? (
@@ -770,11 +790,14 @@ export function DatasetSection() { - setDatasetSliceStart(e.target.value || null) + setDatasetSliceStart(normalizeSliceInput(e.target.value)) } />
@@ -802,11 +825,14 @@ export function DatasetSection() { - setDatasetSliceEnd(e.target.value || null) + setDatasetSliceEnd(normalizeSliceInput(e.target.value)) } />
diff --git a/studio/frontend/src/features/training/api/mappers.ts b/studio/frontend/src/features/training/api/mappers.ts index f9fb07d6dc..b10c282113 100644 --- a/studio/frontend/src/features/training/api/mappers.ts +++ b/studio/frontend/src/features/training/api/mappers.ts @@ -12,7 +12,7 @@ function parseSliceValue(value: string | null): number | null { const trimmed = value.trim(); if (!trimmed) return null; const num = Number(trimmed); - if (!Number.isFinite(num) || !Number.isInteger(num)) return null; + if (!Number.isFinite(num) || !Number.isInteger(num) || num < 0) return null; return num; } diff --git a/studio/frontend/src/features/training/api/models-api.ts b/studio/frontend/src/features/training/api/models-api.ts index 64ac3884e3..ca5bdc7bf7 100644 --- a/studio/frontend/src/features/training/api/models-api.ts +++ b/studio/frontend/src/features/training/api/models-api.ts @@ -67,8 +67,8 @@ export interface ModelConfigResponse { config?: BackendModelConfig | null; is_vision: boolean; is_embedding?: boolean; + is_audio: boolean; is_lora: boolean; - is_audio?: boolean; base_model?: string | null; model_type?: "text" | "vision" | "audio" | "embeddings" | null; } diff --git a/studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx b/studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx index 0dab4685be..b177a41999 100644 --- a/studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx +++ b/studio/frontend/src/features/training/components/hf-dataset-subset-split-selectors.tsx @@ -56,6 +56,8 @@ export function HfDatasetSubsetSplitSelectors({ } = useHfDatasetSplits(enabled ? datasetName : null, datasetSubset, { accessToken, }); + const showPlaceholderDropdowns = + variant === "studio" && !enabled && !datasetName; // Auto-select subset and split in one pass to avoid racing effects useEffect(() => { @@ -83,12 +85,48 @@ export function HfDatasetSubsetSplitSelectors({ setDatasetSplit, ]); - if (!enabled || !datasetName) return null; - const showDropdowns = !isLoading && !error && hfSubsets.length > 0; return ( <> + {showPlaceholderDropdowns && ( + <> +
+ + +
+ + + )} + {isLoading && (
@@ -217,8 +260,9 @@ function SelectorDropdown({ onChange(v === "_none" ? null : v)} + disabled={disabled} > diff --git a/studio/frontend/src/features/training/stores/training-config-store.ts b/studio/frontend/src/features/training/stores/training-config-store.ts index 4ee25ca668..56460137a9 100644 --- a/studio/frontend/src/features/training/stores/training-config-store.ts +++ b/studio/frontend/src/features/training/stores/training-config-store.ts @@ -37,6 +37,7 @@ const initialState: TrainingConfigState = { isCheckingVision: false, isVisionModel: false, isEmbeddingModel: false, + isAudioModel: false, isLoadingModelDefaults: false, modelDefaultsError: null, modelDefaultsAppliedFor: null, @@ -60,6 +61,7 @@ const NON_PERSISTED_STATE_KEYS: ReadonlySet = new Set "modelType", "isCheckingVision", "isEmbeddingModel", + "isAudioModel", "isLoadingModelDefaults", "modelDefaultsError", "modelDefaultsAppliedFor", @@ -129,6 +131,16 @@ export const useTrainingConfigStore = create()( patch.trainOnCompletions = false; } + const isAudio = !!modelDetails.is_audio; + // Pure audio model → always uncheck trainOnCompletions. + if (isAudio && !modelDetails.is_vision) { + patch.trainOnCompletions = false; + } + // Audio-capable vision model (e.g. gemma3n) + audio dataset → uncheck. + if (isAudio && modelDetails.is_vision && get().isDatasetAudio) { + patch.trainOnCompletions = false; + } + // Use backend-provided model_type when available, otherwise // infer from capability flags. const isEmbedding = !!modelDetails.is_embedding; @@ -140,6 +152,7 @@ export const useTrainingConfigStore = create()( modelType: inferredModelType, isVisionModel: modelDetails.is_vision, isEmbeddingModel: isEmbedding, + isAudioModel: isAudio, isLoadingModelDefaults: false, isCheckingVision: false, modelDefaultsError: null, @@ -152,6 +165,7 @@ export const useTrainingConfigStore = create()( set({ isLoadingModelDefaults: false, + isAudioModel: false, modelDefaultsError: error instanceof Error ? error.message @@ -165,12 +179,13 @@ export const useTrainingConfigStore = create()( set({ modelType: isVision ? "vision" : "text", isVisionModel: isVision, + isAudioModel: false, isCheckingVision: false, }); }) .catch(() => { if (get().selectedModel !== modelName) return; - set({ isCheckingVision: false }); + set({ isCheckingVision: false, isAudioModel: false }); }); }); }; @@ -198,10 +213,18 @@ export const useTrainingConfigStore = create()( isCheckingDataset: false, }; if (!_trainOnCompletionsManuallySet) { - const { isVisionModel } = get(); + const { isVisionModel, isAudioModel } = get(); if (isVisionModel && isImage) { updates.trainOnCompletions = false; } + // Pure audio model → always uncheck regardless of dataset. + if (isAudioModel && !isVisionModel) { + updates.trainOnCompletions = false; + } + // Audio-capable vision model (e.g. gemma3n) + audio dataset → uncheck. + if (isAudioModel && isVisionModel && isAudio) { + updates.trainOnCompletions = false; + } } set(updates); }) @@ -238,6 +261,7 @@ export const useTrainingConfigStore = create()( isCheckingVision: false, isVisionModel: false, isEmbeddingModel: false, + isAudioModel: false, isDatasetAudio: false, isLoadingModelDefaults: false, modelDefaultsError: null, @@ -255,6 +279,7 @@ export const useTrainingConfigStore = create()( isCheckingVision: false, isVisionModel: false, isEmbeddingModel: false, + isAudioModel: false, isDatasetAudio: false, isLoadingModelDefaults: false, modelDefaultsError: null, diff --git a/studio/frontend/src/features/training/types/config.ts b/studio/frontend/src/features/training/types/config.ts index 66d5e339ce..32069baa68 100644 --- a/studio/frontend/src/features/training/types/config.ts +++ b/studio/frontend/src/features/training/types/config.ts @@ -61,6 +61,7 @@ export interface TrainingConfigState { isCheckingVision: boolean; isVisionModel: boolean; isEmbeddingModel: boolean; + isAudioModel: boolean; isLoadingModelDefaults: boolean; modelDefaultsError: string | null; modelDefaultsAppliedFor: string | null;