Repository: facebookresearch/hydra

Issue:
[Bug] Joblib launcher plugin doesn't properly convert some joblib arguments to integers
# 🐛 Bug
## Description

The following `joblib.Parallel` arguments are currently provided as strings to joblib when they should be integers:
* `pre_dispatch`
* `batch_size`
* `max_nbytes`

## Checklist
- [X] I checked on the latest version of Hydra
- [X] I created a minimal repro

## To reproduce

** Minimal Code/Config snippet to reproduce **

```bash
wget https://raw.githubusercontent.com/facebookresearch/hydra/master/examples/tutorials/basic/your_first_hydra_app/1_simple_cli/my_app.py
python my_app.py hydra.launcher.batch_size=1 hydra/launcher=joblib -m
```

** Stack trace/error message **
```
[2020-10-27 18:20:48,363][HYDRA] Joblib.Parallel(n_jobs=-1,backend=loky,prefer=processes,require=None,verbose=0,timeout=None,pre_dispatch=2*n_jobs,batch_size=1,temp_folder=None,max_nbytes=None,mmap_mode=r) is launching 1 jobs
[2020-10-27 18:20:48,363][HYDRA] Launching jobs, sweep output dir : multirun/2020-10-27/18-20-48
[2020-10-27 18:20:48,363][HYDRA]        #0 :
Traceback (most recent call last):
  File "/Users/odelalleau/src/hydra/hydra/_internal/utils.py", line 207, in run_and_report
    return func()
  File "/Users/odelalleau/src/hydra/hydra/_internal/utils.py", line 364, in <lambda>
    lambda: hydra.multirun(
  File "/Users/odelalleau/src/hydra/hydra/_internal/hydra.py", line 136, in multirun
    return sweeper.sweep(arguments=task_overrides)
  File "/Users/odelalleau/src/hydra/hydra/_internal/core_plugins/basic_sweeper.py", line 154, in sweep
    results = self.launcher.launch(batch, initial_job_idx=initial_job_idx)
  File "/Users/odelalleau/src/hydra/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/joblib_launcher.py", line 45, in launch
    return _core.launch(
  File "/Users/odelalleau/src/hydra/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/_core.py", line 89, in launch
    runs = Parallel(**joblib_cfg)(
  File "/Users/odelalleau/.local/lib/python3.8/site-packages/joblib/parallel.py", line 715, in __init__
    raise ValueError(
ValueError: batch_size must be 'auto' or a positive integer, got: '1'
```

(with `max_nbytes` it also crashes due to the string issue, `pre_dispatch` actually runs but not sure what happens exactly since it's given as a string)

## Expected Behavior

Running without error, providing appropriate integer arguments to `joblib.Parallel` when they should be integers (obviously there are situations where they should remain strings).

## System information
- **Hydra Version** :  master
- **Python version** : 3.8.5
- **Virtual environment type and version** : conda 4.8.5
- **Operating system** : MacOS



Discussion:
Thx for reporting @odelalleau. will take a look at this today or tomorrow. 
@omry 
Since struct config is not supporting `Union` types for now,  and for several of the fields we need `Union[str, int]`, is there any better alternative to just do `Any` for the fields?

I think string and manually convert to integer if it's a number before passing to joblib.
> I think string and manually convert to integer if it's a number before passing to joblib.

sounds good. I will also update the documentation to make it more clear. 

Resolving patch:
diff --git a/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/_core.py b/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/_core.py
--- a/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/_core.py
+++ b/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/_core.py
@@ -50,6 +50,17 @@ def execute_job(
     return ret
 
 
+def process_joblib_cfg(joblib_cfg: Dict[str, Any]) -> None:
+    for k in ["pre_dispatch", "batch_size", "max_nbytes"]:
+        if k in joblib_cfg.keys():
+            try:
+                val = joblib_cfg.get(k)
+                if val:
+                    joblib_cfg[k] = int(val)
+            except ValueError:
+                pass
+
+
 def launch(
     launcher: JoblibLauncher,
     job_overrides: Sequence[Sequence[str]],
@@ -73,6 +84,7 @@ def launch(
     # backend is incompatible with Hydra
     joblib_cfg = launcher.joblib
     joblib_cfg["backend"] = "loky"
+    process_joblib_cfg(joblib_cfg)
 
     log.info(
         "Joblib.Parallel({}) is launching {} jobs".format(
diff --git a/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/config.py b/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/config.py
--- a/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/config.py
+++ b/plugins/hydra_joblib_launcher/hydra_plugins/hydra_joblib_launcher/config.py
@@ -24,8 +24,8 @@ class JobLibLauncherConf:
     # if greater than zero, prints progress messages
     verbose: int = 0
 
-    # timeout limit for each task
-    timeout: Optional[int] = None
+    # timeout limit for each task. Unit dependent on backend implementation; miliseconds for loky.
+    timeout: Optional[float] = None
 
     # number of batches to be pre-dispatched
     pre_dispatch: str = "2*n_jobs"


Test patch:
diff --git a/plugins/hydra_joblib_launcher/tests/test_joblib_launcher.py b/plugins/hydra_joblib_launcher/tests/test_joblib_launcher.py
--- a/plugins/hydra_joblib_launcher/tests/test_joblib_launcher.py
+++ b/plugins/hydra_joblib_launcher/tests/test_joblib_launcher.py
@@ -69,3 +69,28 @@ def test_example_app(hydra_sweep_runner: TSweepRunner, tmpdir: Any) -> None:
         assert sweep.returns is not None and len(sweep.returns[0]) == 4
         for ret in sweep.returns[0]:
             assert tuple(ret.overrides) in overrides
+
+
+@pytest.mark.parametrize(  # type: ignore
+    "overrides",
+    [
+        "hydra.launcher.batch_size=1",
+        "hydra.launcher.max_nbytes=10000",
+        "hydra.launcher.max_nbytes=1M",
+        "hydra.launcher.pre_dispatch=all",
+        "hydra.launcher.pre_dispatch=10",
+        "hydra.launcher.pre_dispatch=3*n_jobs",
+    ],
+)
+def test_example_app_launcher_overrides(
+    hydra_sweep_runner: TSweepRunner, overrides: str
+) -> None:
+    with hydra_sweep_runner(
+        calling_file="example/my_app.py",
+        calling_module=None,
+        task_function=None,
+        config_path=None,
+        config_name="config",
+        overrides=[overrides],
+    ) as sweep:
+        assert sweep.returns is not None and len(sweep.returns[0]) == 1