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
as `gpus`, exported as `CUDA_VISIBLE_DEVICES`, and reflected into YOLO/visual
`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
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 "gpus" in frontend_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
and "eventSourceRef" 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"
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):
log_path = tmp_path / "job.log"
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 = {
module: string;
package?: string;
@@ -327,6 +334,7 @@ function formatBytes(value?: number) {
function useData() {
const [catalog, setCatalog] = useState<Catalog | null>(null);
const [gpus, setGpus] = useState<GpuPayload | null>(null);
const [condaEnvs, setCondaEnvs] = useState<CondaEnvPayload | null>(null);
const [jobs, setJobs] = useState<Job[]>([]);
const [results, setResults] = useState<ResultItem[]>([]);
const [curves, setCurves] = useState<TrainingCurve[]>([]);
@@ -342,9 +350,10 @@ function useData() {
async function refresh() {
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<GpuPayload>("/api/system/gpus"),
api<CondaEnvPayload>("/api/system/envs"),
api<RuntimeReadinessPayload>("/api/system/readiness"),
api<CapabilityPayload>("/api/capabilities"),
api<Job[]>("/api/jobs"),
@@ -358,6 +367,7 @@ function useData() {
]);
setCatalog(catalogNext);
setGpus(gpusNext);
setCondaEnvs(envsNext);
setRuntimeReadiness(readinessNext);
setCapabilities(capabilitiesNext);
setJobs(jobsNext);
@@ -392,7 +402,7 @@ function useData() {
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 }) {
@@ -415,7 +425,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
}
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 [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
@@ -428,6 +438,7 @@ function App() {
const [resultFamilyFilter, setResultFamilyFilter] = useState("all");
const [resultRoleFilter, setResultRoleFilter] = useState("all");
const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]);
const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto");
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
@@ -482,6 +493,7 @@ function App() {
}, [curves, results, selectedDataset]);
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
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 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]);
@@ -522,11 +534,12 @@ function App() {
}
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,
params: paramsWithGpuSelection(type, rawParams)
};
if (selectedGpuIds.length) payload.gpus = selectedGpuIds;
if (selectedCondaEnv !== "auto") payload.conda_env = selectedCondaEnv;
return payload;
}
@@ -889,6 +902,18 @@ function App() {
))}
</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">
<span> JSON</span>
<textarea value={params} onChange={(event) => setParams(event.target.value)} />

View File

@@ -473,6 +473,34 @@ h2 {
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 {
display: grid;
gap: 8px;