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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user