Add conda environment selector
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user