Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions flask-server/scripts/lesionseg_predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,42 @@ def _resampling_fn_probabilities(self):
reader_writer = SimpleITKIO()
preprocessor = DefaultPreprocessor(verbose=False)

def _export_gpu_resident(logits, props):
"""Turn logits into the final segmentation without the CPU round trip.

predict_logits_from_preprocessed_data returns logits on the CPU, so the default
convert_logits path moves the ~1.5 GB, 43-channel volume CPU->GPU to run the
(already GPU-patched) export resample, then back again. Keeping it resident on the
GPU -- same resample function, same spacings, same argmax -- removes that transfer
and the CPU-side handling. Measured bit-identical to convert_logits (0 differing
voxels across a size-stratified sample, including anisotropic cases) at ~3.6-25x on
the export stage.

Only valid for a plain softmax-argmax label space; region-based models need the
label manager's own logic, so those fall back to convert_logits.
"""
pm, cm = predictor.plans_manager, predictor.configuration_manager
lg = (logits if torch.is_tensor(logits) else torch.from_numpy(logits)).to("cuda")
# spacings, exactly as convert_logits computes them
spacing_t = [props["spacing"][i] for i in pm.transpose_forward]
target = props["shape_after_cropping_and_before_resampling"]
current_spacing = cm.spacing if len(cm.spacing) == len(target) else [spacing_t[0], *cm.spacing]
new_spacing = [props["spacing"][i] for i in pm.transpose_forward]
resampled = cm.resampling_fn_probabilities(lg, target, current_spacing, new_spacing)
if not torch.is_tensor(resampled):
resampled = torch.as_tensor(resampled, device="cuda")
# non-region model: convert_logits_to_segmentation is argmax over channels, and
# argmax(logits) == argmax(softmax(logits)) since softmax is monotonic
seg = resampled.argmax(0).to(torch.uint8)
out = torch.zeros(tuple(props["shape_before_cropping"]), dtype=torch.uint8, device=seg.device)
out[tuple(slice(b[0], b[1]) for b in props["bbox_used_for_cropping"])] = seg
out_np = out.cpu().numpy().transpose(pm.transpose_backward)
del lg, resampled, seg, out
torch.cuda.empty_cache()
return out_np

use_gpu_export = not getattr(predictor.label_manager, "has_regions", False)

case_files = sorted(
f for f in os.listdir(args.input_dir)
if f.endswith("_0000.nii.gz") or f.endswith("_0000.nii")
Expand All @@ -169,10 +205,13 @@ def _resampling_fn_probabilities(self):
predictor.plans_manager, predictor.configuration_manager, predictor.dataset_json,
)
logits = predictor.predict_logits_from_preprocessed_data(torch.from_numpy(preprocessed))
segmentation = convert_logits(
logits, predictor.plans_manager, predictor.configuration_manager,
predictor.label_manager, preprocessed_props, False,
)
if use_gpu_export:
segmentation = _export_gpu_resident(logits, preprocessed_props)
else:
segmentation = convert_logits(
logits, predictor.plans_manager, predictor.configuration_manager,
predictor.label_manager, preprocessed_props, False,
)
reader_writer.write_seg(
segmentation, os.path.join(args.output_dir, f"{case_id}.nii.gz"), preprocessed_props
)
Expand Down
Loading