Repository: voxel51/fiftyone
Issue: [BUG] dataset.apply_model AttributeError: Can't pickle local object '_make_data_loader.<locals>.collate_fn'

### System information

-   **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Windows 10
-   **FiftyOne installed from (pip or source)**: pip
-   **FiftyOne version (run `fiftyone --version`)**: 0.14.2
-   **Python version**: 3.9.9

### Commands to reproduce

### Describe the problem

Following fiftyone.examples, Uncaught exception when attempting to dataset.apply_model()

### Code to reproduce issue

```py
# from fiftyone.examples
import torch
import fiftyone as fo
import fiftyone.zoo as foz

dataset = foz.load_zoo_dataset("coco-2017", split="validation")
session = fo.launch_app(dataset)
model = foz.load_zoo_model("faster-rcnn-resnet50-fpn-coco-torch")
dataset.apply_model(model, label_field="predictions", confidence_thresh=0.6)
```

### Other info / logs

```py
dataset.apply_model(model, label_field="predictions", confidence_thresh=0.6)
```

```
Uncaught exception
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\\Python\\Python39\\lib\\multiprocessing\\spawn.py", line 116, in spawn_main
    exitcode = _main(fd, parent_sentinel)
  File "C:\\Python\\Python39\\lib\\multiprocessing\\spawn.py", line 126, in _main
    self = reduction.pickle.load(from_parent)
EOFError: Ran out of input
   0% ||---------------------------------------------------------------------------------------------------------------------|    0/5000 [4.9s elapsed, ? remaining, ? samples/s]
Uncaught exception
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\\FiftyOne\\venv\\lib\\site-packages\\fiftyone\\core\\collections.py", line 1531, in apply_model
    fomo.apply_model(
  File "D:\\FiftyOne\\venv\\lib\\site-packages\\fiftyone\\core\\models.py", line 188, in apply_model
    return _apply_image_model_data_loader(
  File "D:\\FiftyOne\\venv\\lib\\site-packages\\fiftyone\\core\\models.py", line 294, in _apply_image_model_data_loader
    for sample_batch, imgs in zip(samples_loader, data_loader):
  File "D:\\FiftyOne\\venv\\lib\\site-packages\\torch\\utils\\data\\dataloader.py", line 359, in __iter__
    return self._get_iterator()
  File "D:\\FiftyOne\\venv\\lib\\site-packages\\torch\\utils\\data\\dataloader.py", line 305, in _get_iterator
    return _MultiProcessingDataLoaderIter(self)
  File "D:\\FiftyOne\\venv\\lib\\site-packages\\torch\\utils\\data\\dataloader.py", line 918, in __init__
    w.start()
  File "C:\\Python\\Python39\\lib\\multiprocessing\\process.py", line 121, in start
    self._popen = self._Popen(self)
  File "C:\\Python\\Python39\\lib\\multiprocessing\\context.py", line 224, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:\\Python\\Python39\\lib\\multiprocessing\\context.py", line 327, in _Popen
    return Popen(process_obj)
  File "C:\\Python\\Python39\\lib\\multiprocessing\\popen_spawn_win32.py", line 93, in __init__
    reduction.dump(process_obj, to_child)
  File "C:\\Python\\Python39\\lib\\multiprocessing\\reduction.py", line 60, in dump
    ForkingPickler(file, protocol).dump(obj)
AttributeError: Can't pickle local object '_make_data_loader.<locals>.collate_fn'
```

### What areas of FiftyOne does this bug affect?

-   [ ] `App`: FiftyOne application issue
-   [x ] `Core`: Core `fiftyone` Python library issue
-   [ ] `Server`: Fiftyone server issue

### Willingness to contribute

The FiftyOne Community encourages bug fix contributions. Would you or another
member of your organization be willing to contribute a fix for this bug to the
FiftyOne codebase?

-   [ ] Yes. I can contribute a fix for this bug independently.
-   [ ] Yes. I would be willing to contribute a fix for this bug with guidance
        from the FiftyOne community.
-   [ x] No. I cannot contribute a bug fix at this time.


Changed files:
fiftyone/utils/image.py, fiftyone/utils/data/base.py, fiftyone/utils/coco.py, fiftyone/utils/cvat.py, fiftyone/utils/aws.py, fiftyone/core/metadata.py, fiftyone/utils/torch.py

diff --git a/fiftyone/core/metadata.py b/fiftyone/core/metadata.py
index 6eca9958a..93febcd45 100644
--- a/fiftyone/core/metadata.py
+++ b/fiftyone/core/metadata.py
@@ -246,7 +246,7 @@ def compute_metadata(
     if num_workers is None:
         num_workers = multiprocessing.cpu_count()
 
-    if num_workers == 1:
+    if num_workers <= 1:
         _compute_metadata(sample_collection, overwrite=overwrite)
     else:
         _compute_metadata_multi(
diff --git a/fiftyone/utils/aws.py b/fiftyone/utils/aws.py
index ccc6dc3e5..db2fe1e48 100644
--- a/fiftyone/utils/aws.py
+++ b/fiftyone/utils/aws.py
@@ -66,7 +66,7 @@ def download_public_s3_files(
     if download_dir:
         etau.ensure_dir(download_dir)
 
-    if num_workers is None or num_workers < 1:
+    if num_workers is None:
         num_workers = multiprocessing.cpu_count()
 
     s3_client = boto3.client(
@@ -84,7 +84,7 @@ def download_public_s3_files(
     if not inputs:
         return
 
-    if num_workers == 1:
+    if num_workers <= 1:
         _single_thread_download(inputs)
     else:
         _multi_thread_download(inputs, num_workers)
diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py
index ae8b1b478..e8779e7f0 100644
--- a/fiftyone/utils/coco.py
+++ b/fiftyone/utils/coco.py
@@ -1824,7 +1824,7 @@ def _get_existing_ids(images_dir, images, image_ids):
 
 
 def _download_images(images_dir, image_ids, images, num_workers):
-    if num_workers is None or num_workers < 1:
+    if num_workers is None:
         num_workers = multiprocessing.cpu_count()
 
     tasks = []
@@ -1837,7 +1837,7 @@ def _download_images(images_dir, image_ids, images, num_workers):
     if not tasks:
         return
 
-    if num_workers == 1:
+    if num_workers <= 1:
         with fou.ProgressBar(iters_str="images") as pb:
             for task in pb(tasks):
                 _do_download(task)
diff --git a/fiftyone/utils/cvat.py b/fiftyone/utils/cvat.py
index c31a742de..59013811e 100644
--- a/fiftyone/utils/cvat.py
+++ b/fiftyone/utils/cvat.py
@@ -311,11 +311,11 @@ def _parse_task_metadata(
 
 
 def _download_media(tasks, num_workers):
-    if num_workers is None or num_workers < 1:
+    if num_workers is None:
         num_workers = multiprocessing.cpu_count()
 
     logger.info("Downloading media...")
-    if num_workers == 1:
+    if num_workers <= 1:
         with fou.ProgressBar() as pb:
             for task in pb(tasks):
                 _do_download_media(task)
diff --git a/fiftyone/utils/data/base.py b/fiftyone/utils/data/base.py
index a5f46b310..83b9d860e 100644
--- a/fiftyone/utils/data/base.py
+++ b/fiftyone/utils/data/base.py
@@ -175,7 +175,7 @@ def download_images(image_urls, output_dir, num_workers=None):
         outpath = os.path.join(output_dir, filename)
         inputs.append((url, outpath))
 
-    if num_workers == 1:
+    if num_workers <= 1:
         _download_images(inputs)
     else:
         _download_images_multi(inputs, num_workers)
diff --git a/fiftyone/utils/image.py b/fiftyone/utils/image.py
index 21043c43f..7002c54d3 100644
--- a/fiftyone/utils/image.py
+++ b/fiftyone/utils/image.py
@@ -47,7 +47,7 @@ def reencode_images(
     if num_workers is None:
         num_workers = multiprocessing.cpu_count()
 
-    if num_workers == 1:
+    if num_workers <= 1:
         _transform_images(
             sample_collection,
             ext=ext,
@@ -113,7 +113,7 @@ def transform_images(
     if num_workers is None:
         num_workers = multiprocessing.cpu_count()
 
-    if num_workers == 1:
+    if num_workers <= 1:
         _transform_images(
             sample_collection,
             size=size,
diff --git a/fiftyone/utils/torch.py b/fiftyone/utils/torch.py
index a8b9894ac..dc0c6c6da 100644
--- a/fiftyone/utils/torch.py
+++ b/fiftyone/utils/torch.py
@@ -935,8 +935,14 @@ def recommend_num_workers():
     :class:`torch:torch.utils.data.DataLoader`.
 
     Returns:
-        the recommended ``num_workers``
+        the recommended number of workers
     """
+    if sys.platform.startswith("win"):
+        # Windows tends to have multiprocessing issues, so default to 0 workers
+        # https://github.com/voxel51/fiftyone/issues/1531
+        # https://stackoverflow.com/q/20222534
+        return 0
+
     if sys.platform == "darwin" and not torch.cuda.is_available():
         # There is a parallelism bug on macOS with CPU that prevents us from
         # using `num_workers > 0`