Add conda environment selector

This commit is contained in:
2026-06-30 16:01:41 +08:00
parent 73d15e9dce
commit e482651545
5 changed files with 79 additions and 4 deletions

View File

@@ -71,6 +71,10 @@ The task builder also reads `GET /api/system/gpus` and lets an operator choose
CPU or one or more GPUs before launch. Selected GPUs are passed to the backend CPU or one or more GPUs before launch. Selected GPUs are passed to the backend
as `gpus`, exported as `CUDA_VISIBLE_DEVICES`, and reflected into YOLO/visual as `gpus`, exported as `CUDA_VISIBLE_DEVICES`, and reflected into YOLO/visual
`device` parameters and MMSeg config-generation `gpu_count/gpu_ids`. `device` parameters and MMSeg config-generation `gpu_count/gpu_ids`.
The same job launcher reads `GET /api/system/envs` and provides an Auto/manual
conda environment selector. Auto keeps the backend defaults (`seg_smp` for
general SegModel/YOLO/dataset tasks and `seg_mmcv` for MMSeg); manual mode
sends `conda_env` with the job request for custom algorithm environments.
The coverage panel calls `GET /api/coverage` and verifies that the user-facing The coverage panel calls `GET /api/coverage` and verifies that the user-facing
scripts from the existing `Seg/` workspace are mapped to web jobs. MMSeg scripts from the existing `Seg/` workspace are mapped to web jobs. MMSeg

View File

@@ -68,6 +68,11 @@ def evaluate_project() -> dict:
and "gpu_count" in frontend_text and "gpu_count" in frontend_text
and "gpus" in frontend_text and "gpus" in frontend_text
and "CUDA_VISIBLE_DEVICES" in jobs_text, and "CUDA_VISIBLE_DEVICES" in jobs_text,
"conda_env_selection_ui": "/api/system/envs" in frontend_text
and "selectedCondaEnv" in frontend_text
and "runtimeSelector" in frontend_text
and "conda_env" in frontend_text
and "request.conda_env" in jobs_text,
"live_log_stream_ui": "EventSource" in frontend_text "live_log_stream_ui": "EventSource" in frontend_text
and "eventSourceRef" in frontend_text and "eventSourceRef" in frontend_text
and "log_size" in frontend_text and "log_size" in frontend_text

View File

@@ -22,6 +22,19 @@ def test_build_task_sets_cuda_visible_devices(tmp_path, monkeypatch):
assert spec.env["CUDA_VISIBLE_DEVICES"] == "2,0" assert spec.env["CUDA_VISIBLE_DEVICES"] == "2,0"
def test_build_task_uses_requested_conda_env(tmp_path, monkeypatch):
captured = {}
def fake_build_module_task(job_type, params, conda_env):
captured["conda_env"] = conda_env
return CommandSpec(["python", "-c", "print('ok')"], tmp_path, "fake task")
monkeypatch.setattr(jobs, "build_module_task", fake_build_module_task)
jobs._build_task(JobCreate(type="segmodel.train", params={}, conda_env="seg_custom"))
assert captured["conda_env"] == "seg_custom"
def test_job_progress_reports_log_size(tmp_path): def test_job_progress_reports_log_size(tmp_path):
log_path = tmp_path / "job.log" log_path = tmp_path / "job.log"
log_path.write_text("line one\nline two\n", encoding="utf-8") log_path.write_text("line one\nline two\n", encoding="utf-8")

View File

@@ -167,6 +167,13 @@ type GpuPayload = {
}>; }>;
}; };
type CondaEnvPayload = {
available: boolean;
envs: Array<{ name: string; path: string; active?: boolean }>;
task_default?: string;
mmseg_default?: string;
};
type RuntimeCheck = { type RuntimeCheck = {
module: string; module: string;
package?: string; package?: string;
@@ -327,6 +334,7 @@ function formatBytes(value?: number) {
function useData() { function useData() {
const [catalog, setCatalog] = useState<Catalog | null>(null); const [catalog, setCatalog] = useState<Catalog | null>(null);
const [gpus, setGpus] = useState<GpuPayload | null>(null); const [gpus, setGpus] = useState<GpuPayload | null>(null);
const [condaEnvs, setCondaEnvs] = useState<CondaEnvPayload | null>(null);
const [jobs, setJobs] = useState<Job[]>([]); const [jobs, setJobs] = useState<Job[]>([]);
const [results, setResults] = useState<ResultItem[]>([]); const [results, setResults] = useState<ResultItem[]>([]);
const [curves, setCurves] = useState<TrainingCurve[]>([]); const [curves, setCurves] = useState<TrainingCurve[]>([]);
@@ -342,9 +350,10 @@ function useData() {
async function refresh() { async function refresh() {
try { try {
const [catalogNext, gpusNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([ const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([
api<Catalog>("/api/catalog"), api<Catalog>("/api/catalog"),
api<GpuPayload>("/api/system/gpus"), api<GpuPayload>("/api/system/gpus"),
api<CondaEnvPayload>("/api/system/envs"),
api<RuntimeReadinessPayload>("/api/system/readiness"), api<RuntimeReadinessPayload>("/api/system/readiness"),
api<CapabilityPayload>("/api/capabilities"), api<CapabilityPayload>("/api/capabilities"),
api<Job[]>("/api/jobs"), api<Job[]>("/api/jobs"),
@@ -358,6 +367,7 @@ function useData() {
]); ]);
setCatalog(catalogNext); setCatalog(catalogNext);
setGpus(gpusNext); setGpus(gpusNext);
setCondaEnvs(envsNext);
setRuntimeReadiness(readinessNext); setRuntimeReadiness(readinessNext);
setCapabilities(capabilitiesNext); setCapabilities(capabilitiesNext);
setJobs(jobsNext); setJobs(jobsNext);
@@ -392,7 +402,7 @@ function useData() {
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, []); }, []);
return { catalog, gpus, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh }; return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
} }
function StatusPill({ status }: { status: string }) { function StatusPill({ status }: { status: string }) {
@@ -415,7 +425,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
} }
function App() { function App() {
const { catalog, gpus, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData(); const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo"); const [taskType, setTaskType] = useState("mock.echo");
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null); const [selectedJob, setSelectedJob] = useState<Job | null>(null);
@@ -428,6 +438,7 @@ function App() {
const [resultFamilyFilter, setResultFamilyFilter] = useState("all"); const [resultFamilyFilter, setResultFamilyFilter] = useState("all");
const [resultRoleFilter, setResultRoleFilter] = useState("all"); const [resultRoleFilter, setResultRoleFilter] = useState("all");
const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]); const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]);
const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto");
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images"); const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null); const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null); const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
@@ -482,6 +493,7 @@ function App() {
}, [curves, results, selectedDataset]); }, [curves, results, selectedDataset]);
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight); const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
const availableGpus = gpus?.gpus ?? []; const availableGpus = gpus?.gpus ?? [];
const condaEnvOptions = useMemo(() => ["auto", ...Array.from(new Set((condaEnvs?.envs ?? []).map((item) => item.name))).sort()], [condaEnvs]);
const selectedGpuDevice = selectedGpuIds.length ? selectedGpuIds.map((_, index) => index).join(",") : "cpu"; const selectedGpuDevice = selectedGpuIds.length ? selectedGpuIds.map((_, index) => index).join(",") : "cpu";
const resultFamilyOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.family ?? "artifact"))).sort()], [results]); const resultFamilyOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.family ?? "artifact"))).sort()], [results]);
const resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]); const resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]);
@@ -522,11 +534,12 @@ function App() {
} }
function jobPayload(type: string, rawParams: Record<string, unknown>) { function jobPayload(type: string, rawParams: Record<string, unknown>) {
const payload: { type: string; params: Record<string, unknown>; gpus?: number[] } = { const payload: { type: string; params: Record<string, unknown>; gpus?: number[]; conda_env?: string } = {
type, type,
params: paramsWithGpuSelection(type, rawParams) params: paramsWithGpuSelection(type, rawParams)
}; };
if (selectedGpuIds.length) payload.gpus = selectedGpuIds; if (selectedGpuIds.length) payload.gpus = selectedGpuIds;
if (selectedCondaEnv !== "auto") payload.conda_env = selectedCondaEnv;
return payload; return payload;
} }
@@ -889,6 +902,18 @@ function App() {
))} ))}
</div> </div>
</div> </div>
<div className="runtimeSelector">
<label>
<span>Conda </span>
<select value={selectedCondaEnv} onChange={(event) => setSelectedCondaEnv(event.target.value)} aria-label="conda environment">
{condaEnvOptions.map((name) => (
<option key={name} value={name}>
{name === "auto" ? `Auto (${taskType.startsWith("mmseg.") ? condaEnvs?.mmseg_default ?? "seg_mmcv" : condaEnvs?.task_default ?? "seg_smp"})` : name}
</option>
))}
</select>
</label>
</div>
<label className="field"> <label className="field">
<span> JSON</span> <span> JSON</span>
<textarea value={params} onChange={(event) => setParams(event.target.value)} /> <textarea value={params} onChange={(event) => setParams(event.target.value)} />

View File

@@ -473,6 +473,34 @@ h2 {
font-size: 10px; font-size: 10px;
} }
.runtimeSelector {
margin-top: 10px;
padding: 10px;
border: 1px solid var(--line);
border-radius: 7px;
background: #101310;
}
.runtimeSelector label {
display: grid;
gap: 7px;
}
.runtimeSelector span {
color: var(--muted);
font-size: 12px;
}
.runtimeSelector select {
width: 100%;
height: 38px;
padding: 0 10px;
border-radius: 6px;
border: 1px solid var(--line);
background: var(--field);
color: var(--ink);
}
.field { .field {
display: grid; display: grid;
gap: 8px; gap: 8px;