Repository: skypilot-org/skypilot
Pull request: Optimize sky cli
**10x** the sky cli loading speed: ~3.6s -> ~0.36s

The most time consuming pkgs:
```
pandas
ray
networkx
docker
oauth2client
```

Closes #204 

Problem statement:
Hide imports in Sky cli to improve loading speed
Top level imports in Sky cli slows down loading speed. ~Fixing it is not of high priority.~ I do feel it is a bit slow to respond when `sky --help`. However, we need to make sure the slow down is caused by importing modules.

Hints from issue discussion:
+1, this would be nice
```bash
» time sky status
NAME   LAUNCHED    RESOURCES           COMMAND                                    STATUS
debug  2 days ago  1x AWS(m4.2xlarge)  sky launch -c debug examples/minimal.yaml  UP
status() 0.0012319087982177734
sky status  1.62s user 0.28s system 121% cpu 1.566 total
```
The `status()` function itself took 1ms, while the entire invocation took 1.6 seconds (1600x).  +1 to making this a high-priority item.
It seems the importing can affect the responding time a lot. Just saw a [PR](https://github.com/ray-project/ray/pull/21661) in ray that discusses about delaying the import of pandas and some of the ray modules to speed up the UX.

Commit message: fix

Patch to review:
diff --git a/prototype/sky/backends/cloud_vm_ray_backend.py b/prototype/sky/backends/cloud_vm_ray_backend.py
index 160bf737822..36b48aff88c 100644
--- a/prototype/sky/backends/cloud_vm_ray_backend.py
+++ b/prototype/sky/backends/cloud_vm_ray_backend.py
@@ -15,7 +15,6 @@
 from typing import Any, Callable, Dict, List, Optional, Tuple
 
 import colorama
-from ray.autoscaler import sdk
 
 import sky
 from sky import backends
@@ -1288,6 +1287,7 @@ def sync_down_logs(self, handle: ResourceHandle, job_id: int) -> None:
         ips = self._get_node_ips(handle.cluster_yaml, handle.launched_nodes)
 
         def rsync_down(ip: str) -> None:
+            from ray.autoscaler import sdk
             sdk.rsync(
                 handle.cluster_yaml,
                 source=f'{remote_log_dir}/*',
diff --git a/prototype/sky/backends/docker_utils.py b/prototype/sky/backends/docker_utils.py
index 9b57121bb10..c4f6a66e0af 100644
--- a/prototype/sky/backends/docker_utils.py
+++ b/prototype/sky/backends/docker_utils.py
@@ -7,8 +7,8 @@
 from typing import Optional
 
 import colorama
-import docker
 
+from sky import docker_adaptor
 from sky import sky_logging
 from sky import task as task_mod
 
@@ -122,11 +122,11 @@ def _execute_build(tag, context_path):
     """
     assert tag is not None, 'Image tag cannot be None - have you specified a ' \
                             'task name? '
-    docker_client = docker.from_env()
+    docker_client = docker_adaptor.from_env()
     try:
         unused_image, unused_build_logs = docker_client.images.build(
             path=context_path, tag=tag, rm=True, quiet=False)
-    except docker.errors.BuildError as e:
+    except docker_adaptor.build_error() as e:
         colorama.init()
         style = colorama.Style
         fore = colorama.Fore
diff --git a/prototype/sky/backends/local_docker_backend.py b/prototype/sky/backends/local_docker_backend.py
index bb8762c61a2..96fd48dc3dd 100644
--- a/prototype/sky/backends/local_docker_backend.py
+++ b/prototype/sky/backends/local_docker_backend.py
@@ -4,9 +4,9 @@
 from typing import Any, Callable, Dict, Optional, Union
 
 import colorama
-import docker
 
 from sky import backends
+from sky import docker_adaptor
 from sky import global_user_state
 from sky import sky_logging
 from sky import resources
@@ -110,7 +110,7 @@ def __init__(self, use_gpu: Union[bool, str] = 'auto'):
         self.volume_mounts = {}  # Stores the ResourceHandle->volume mounts map
         self.images = {}  # Stores the ResourceHandle->[image_tag, metadata] map
         self.containers = {}
-        self.client = docker.from_env()
+        self.client = docker_adaptor.from_env()
         self._update_state()
 
     def _update_state(self):
@@ -230,7 +230,7 @@ def _run_post_setup(self, handle: ResourceHandle) -> None:
             # Check if a container exists and remove it to create new one
             _ = self.client.containers.get(handle)  # Throws NotFound error
             self.teardown(handle, terminate=True)
-        except docker.errors.NotFound:
+        except docker_adaptor.not_found_error():
             # Container does not exist, we're good to go
             pass
         try:
@@ -244,7 +244,7 @@ def _run_post_setup(self, handle: ResourceHandle) -> None:
                 volumes=volumes,
                 runtime=runtime,
                 labels=labels)
-        except docker.errors.APIError as e:
+        except docker_adaptor.api_error() as e:
             if 'Unknown runtime specified nvidia' in e.explanation:
                 logger.error(
                     'Unable to run container - nvidia runtime for docker not '
diff --git a/prototype/sky/cli.py b/prototype/sky/cli.py
index 7e6759d0ad3..04f49ffec1f 100644
--- a/prototype/sky/cli.py
+++ b/prototype/sky/cli.py
@@ -36,7 +36,6 @@
 import yaml
 
 import click
-import pandas as pd
 import pendulum
 
 import sky
@@ -1361,6 +1360,7 @@ def _output():
         # Show detailed accelerator information
         result = service_catalog.list_accelerators(gpus_only=True,
                                                    name_filter=gpu_name)
+        import pandas as pd
         for i, (gpu, items) in enumerate(result.items()):
             accelerator_table = util_lib.create_table([
                 'GPU',
diff --git a/prototype/sky/clouds/aws.py b/prototype/sky/clouds/aws.py
index 4efe6c1e2dd..4432da4d676 100644
--- a/prototype/sky/clouds/aws.py
+++ b/prototype/sky/clouds/aws.py
@@ -6,7 +6,7 @@
 from typing import Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
 
 from sky import clouds
-from sky.clouds.service_catalog import aws_catalog
+from sky.clouds import service_catalog
 
 if TYPE_CHECKING:
     # renaming to avoid shadowing variables
@@ -78,8 +78,8 @@ def region_zones_provision_loop(
             # fallback to manually specified region/zones
             regions = cls.regions()
         else:
-            regions = aws_catalog.get_region_zones_for_instance_type(
-                instance_type, use_spot)
+            regions = service_catalog.get_region_zones_for_instance_type(
+                instance_type, use_spot, 'aws')
         for region in regions:
             if region.name == 'us-west-1':
                 # TODO: troubles launching AMIs.
@@ -102,7 +102,10 @@ def get_default_ami(cls, region_name: str) -> str:
     #### Normal methods ####
 
     def instance_type_to_hourly_cost(self, instance_type: str, use_spot: bool):
-        return aws_catalog.get_hourly_cost(instance_type, use_spot=use_spot)
+        return service_catalog.get_hourly_cost(instance_type,
+                                               region=None,
+                                               use_spot=use_spot,
+                                               clouds='aws')
 
     def accelerators_to_hourly_cost(self, accelerators):
         # AWS includes accelerators as part of the instance type.  Implementing
@@ -151,7 +154,8 @@ def get_accelerators_from_instance_type(
         self,
         instance_type: str,
     ) -> Optional[Dict[str, int]]:
-        return aws_catalog.get_accelerators_from_instance_type(instance_type)
+        return service_catalog.get_accelerators_from_instance_type(
+            instance_type, clouds='aws')
 
     def make_deploy_resources_variables(self,
                                         resources: 'resources_lib.Resources'):
@@ -193,8 +197,8 @@ def _make(instance_type):
 
         assert len(accelerators) == 1, resources
         acc, acc_count = list(accelerators.items())[0]
-        instance_type = aws_catalog.get_instance_type_for_accelerator(
-            acc, acc_count)
+        instance_type = service_catalog.get_instance_type_for_accelerator(
+            acc, acc_count, clouds='aws')
         if instance_type is None:
             return []
         return _make(instance_type)
diff --git a/prototype/sky/clouds/azure.py b/prototype/sky/clouds/azure.py
index 51a18717b69..ec0c3b278fd 100644
--- a/prototype/sky/clouds/azure.py
+++ b/prototype/sky/clouds/azure.py
@@ -6,7 +6,7 @@
 from typing import Dict, Iterator, List, Optional, Tuple
 
 from sky import clouds
-from sky.clouds.service_catalog import azure_catalog
+from sky.clouds import service_catalog
 
 
 def _run_output(cmd):
@@ -25,7 +25,10 @@ class Azure(clouds.Cloud):
     _regions: List[clouds.Region] = []
 
     def instance_type_to_hourly_cost(self, instance_type, use_spot):
-        return azure_catalog.get_hourly_cost(instance_type, use_spot=use_spot)
+        return service_catalog.get_hourly_cost(instance_type,
+                                               region=None,
+                                               use_spot=use_spot,
+                                               clouds='azure')
 
     def accelerators_to_hourly_cost(self, accelerators):
         # Azure includes accelerators as part of the instance type.
@@ -119,8 +122,8 @@ def region_zones_provision_loop(
             # fallback to manually specified region/zones
             regions = cls.regions()
         else:
-            regions = azure_catalog.get_region_zones_for_instance_type(
-                instance_type, use_spot)
+            regions = service_catalog.get_region_zones_for_instance_type(
+                instance_type, use_spot, clouds='azure')
         for region in regions:
             yield region, region.zones
 
@@ -131,7 +134,8 @@ def get_accelerators_from_instance_type(
         self,
         instance_type: str,
     ) -> Optional[Dict[str, int]]:
-        return azure_catalog.get_accelerators_from_instance_type(instance_type)
+        return service_catalog.get_accelerators_from_instance_type(
+            instance_type, clouds='azure')
 
     def make_deploy_resources_variables(self, resources):
         r = resources
@@ -143,8 +147,8 @@ def make_deploy_resources_variables(self, resources):
             custom_resources = json.dumps(acc_dict, separators=(',', ':'))
         else:
             custom_resources = None
-        gen_version = azure_catalog.get_gen_version_from_instance_type(
-            r.instance_type)
+        gen_version = service_catalog.get_gen_version_from_instance_type(
+            r.instance_type, clouds='azure')
         image_config = self._get_image_config(gen_version, r.instance_type)
         return {
             'instance_type': r.instance_type,
@@ -177,8 +181,8 @@ def _make(instance_type):
 
         assert len(accelerators) == 1, resources
         acc, acc_count = list(accelerators.items())[0]
-        instance_type = azure_catalog.get_instance_type_for_accelerator(
-            acc, acc_count)
+        instance_type = service_catalog.get_instance_type_for_accelerator(
+            acc, acc_count, clouds='azure')
         if instance_type is None:
             return []
         return _make(instance_type)
diff --git a/prototype/sky/clouds/gcp.py b/prototype/sky/clouds/gcp.py
index 64c26b77120..ca83a19a311 100644
--- a/prototype/sky/clouds/gcp.py
+++ b/prototype/sky/clouds/gcp.py
@@ -7,7 +7,7 @@
 from google import auth
 
 from sky import clouds
-from sky.clouds.service_catalog import gcp_catalog
+from sky.clouds import service_catalog
 
 
 class GCP(clouds.Cloud):
@@ -119,8 +119,8 @@ def region_zones_provision_loop(
             assert len(accelerators) == 1, accelerators
             acc = list(accelerators.keys())[0]
             acc_count = list(accelerators.values())[0]
-            regions = gcp_catalog.get_region_zones_for_accelerators(
-                acc, acc_count, use_spot)
+            regions = service_catalog.get_region_zones_for_accelerators(
+                acc, acc_count, use_spot, clouds='gcp')
 
         for region in regions:
             for zone in region.zones:
@@ -136,7 +136,9 @@ def instance_type_to_hourly_cost(self, instance_type, use_spot):
     def accelerators_to_hourly_cost(self, accelerators):
         assert len(accelerators) == 1, accelerators
         acc, acc_count = list(accelerators.items())[0]
-        return gcp_catalog.get_accelerator_hourly_cost(acc, acc_count)
+        return service_catalog.get_accelerator_hourly_cost(acc,
+                                                           acc_count,
+                                                           clouds='gcp')
 
     def get_egress_cost(self, num_gigabytes):
         # In general, query this from the cloud:
@@ -200,7 +202,8 @@ def get_feasible_launchable_resources(self, resources):
             assert resources.is_launchable(), resources
             return [resources]
         if resources.accelerators is not None:
-            available_accelerators = gcp_catalog.list_accelerators()
+            available_accelerators = service_catalog.list_accelerators(
+                gpus_only=False, clouds='gcp')
             for acc, acc_count in resources.accelerators.items():
                 if acc not in available_accelerators or not any(
                         acc_count == info.accelerator_count
diff --git a/prototype/sky/clouds/service_catalog/__init__.py b/prototype/sky/clouds/service_catalog/__init__.py
index 7d8a1cd8ce4..8720a0a600c 100644
--- a/prototype/sky/clouds/service_catalog/__init__.py
+++ b/prototype/sky/clouds/service_catalog/__init__.py
@@ -1,27 +1,57 @@
 """Service catalog."""
 import collections
-from typing import Dict, List, Optional
+import importlib
+from typing import Dict, List, Optional, Union, TYPE_CHECKING
 
-from sky.clouds.service_catalog import aws_catalog
-from sky.clouds.service_catalog import azure_catalog
-from sky.clouds.service_catalog import common
-from sky.clouds.service_catalog import gcp_catalog
+if TYPE_CHECKING:
+    from sky.clouds import cloud
+    from sky.clouds.service_catalog import common
+
+CloudFilter = Optional[Union[List[str], str]]
+ALL_CLOUDS = ('aws', 'azure', 'gcp')
+
+
+def _map_clouds_catalog(clouds: CloudFilter, method_name, *args, **kwargs):
+    if clouds is None:
+        clouds = list(ALL_CLOUDS)
+    single = isinstance(clouds, str)
+    if single:
+        clouds = [clouds]
+
+    results = []
+    for cloud in clouds:
+        try:
+            cloud_module = importlib.import_module(
+                f'sky.clouds.service_catalog.{cloud}_catalog')
+        except ModuleNotFoundError:
+            raise ValueError(
+                'Cannot find module "sky.clouds.service_catalog'
+                f'.{cloud}_catalog" for cloud "{cloud}".') from None
+        try:
+            method = getattr(cloud_module, method_name)
+        except AttributeError:
+            raise AttributeError(f'Cloud "{cloud}" does not implement the '
+                                 f'"{method_name}" method') from None
+        results.append(method(*args, **kwargs))
+    if single:
+        return results[0]
+    return results
 
 
 def list_accelerators(
     gpus_only: bool = True,
     name_filter: Optional[str] = None,
-) -> Dict[str, List[common.InstanceTypeInfo]]:
+    clouds: CloudFilter = None,
+) -> 'Dict[str, List[common.InstanceTypeInfo]]':
     """List the names of all accelerators offered by Sky.
 
     Returns: A dictionary of canonical accelerator names mapped to a list
     of instance type offerings. See usage in cli.py.
     """
-    results = [
-        aws_catalog.list_accelerators(gpus_only, name_filter),
-        azure_catalog.list_accelerators(gpus_only, name_filter),
-        gcp_catalog.list_accelerators(gpus_only, name_filter),
-    ]
+    results = _map_clouds_catalog(clouds, 'list_accelerators', gpus_only,
+                                  name_filter)
+    if not isinstance(results, list):
+        results = [results]
     ret = collections.defaultdict(list)
     for result in results:
         for gpu, items in result.items():
@@ -32,17 +62,17 @@ def list_accelerators(
 def list_accelerator_counts(
     gpus_only: bool = True,
     name_filter: Optional[str] = None,
+    clouds: CloudFilter = None,
 ) -> Dict[str, List[int]]:
     """List all accelerators offered by Sky and available counts.
 
     Returns: A dictionary of canonical accelerator names mapped to a list
     of available counts. See usage in cli.py.
     """
-    results = [
-        aws_catalog.list_accelerators(gpus_only, name_filter),
-        azure_catalog.list_accelerators(gpus_only, name_filter),
-        gcp_catalog.list_accelerators(gpus_only, name_filter),
-    ]
+    results = _map_clouds_catalog(clouds, 'list_accelerators', gpus_only,
+                                  name_filter)
+    if not isinstance(results, list):
+        results = [results]
     ret = collections.defaultdict(set)
     for result in results:
         for gpu, items in result.items():
@@ -53,6 +83,69 @@ def list_accelerator_counts(
     return ret
 
 
+def get_region_zones_for_instance_type(
+        instance_type: str,
+        use_spot: bool,
+        clouds: CloudFilter = None) -> 'List[cloud.Region]':
+    """Returns a list of regions for a given instance type."""
+    return _map_clouds_catalog(clouds, 'get_region_zones_for_instance_type',
+                               instance_type, use_spot)
+
+
+def get_hourly_cost(instance_type: str,
+                    region: Optional[str],
+                    use_spot: bool,
+                    clouds: CloudFilter = None):
+    """Returns the cost, or the cheapest cost among all zones for spot."""
+    return _map_clouds_catalog(clouds, 'get_hourly_cost', instance_type, region,
+                               use_spot)
+
+
+def get_accelerators_from_instance_type(
+        instance_type: str,
+        clouds: CloudFilter = None) -> Optional[Dict[str, int]]:
+    """Returns the accelerators from a instance type."""
+    return _map_clouds_catalog(clouds, 'get_accelerators_from_instance_type',
+                               instance_type)
+
+
+def get_instance_type_for_accelerator(
+    acc_name: str,
+    acc_count: int,
+    clouds: CloudFilter = None,
+) -> Optional[str]:
+    """Returns the instance type with the required count of accelerators."""
+    return _map_clouds_catalog(clouds, 'get_instance_type_for_accelerator',
+                               acc_name, acc_count)
+
+
+def get_accelerator_hourly_cost(
+    acc_name: str,
+    acc_count: int,
+    clouds: CloudFilter = None,
+) -> float:
+    """Returns the hourly cost with the accelerators."""
+    return _map_clouds_catalog(clouds, 'get_accelerator_hourly_cost', acc_name,
+                               acc_count)
+
+
+def get_region_zones_for_accelerators(
+        acc_name: str,
+        acc_count: int,
+        use_spot: bool,
+        clouds: CloudFilter = None) -> 'List[cloud.Region]':
+    """Returns a list of regions for a given accelerators."""
+    return _map_clouds_catalog(clouds, 'get_region_zones_for_accelerators',
+                               acc_name, acc_count, use_spot)
+
+
+def get_gen_version_from_instance_type(instance_type: str,
+                                       clouds: CloudFilter = None
+                                      ) -> Optional[int]:
+    return _map_clouds_catalog(clouds, 'get_gen_version_from_instance_type',
+                               instance_type)
+
+
 def get_common_gpus() -> List[str]:
     """Returns a list of commonly used GPU names."""
     return ['V100', 'V100-32GB', 'A100', 'P100', 'K80', 'T4', 'M60']
@@ -64,11 +157,14 @@ def get_tpus() -> List[str]:
 
 
 __all__ = [
-    'aws_catalog',
-    'azure_catalog',
-    'get_common_gpus',
-    'get_tpus',
-    'gcp_catalog',
     'list_accelerators',
     'list_accelerator_counts',
+    'get_region_zones_for_instance_type',
+    'get_hourly_cost',
+    'get_accelerators_from_instance_type',
+    'get_instance_type_for_accelerator',
+    'get_accelerator_hourly_cost',
+    'get_region_zones_for_accelerators',
+    'get_common_gpus',
+    'get_tpus',
 ]
diff --git a/prototype/sky/clouds/service_catalog/aws_catalog.py b/prototype/sky/clouds/service_catalog/aws_catalog.py
index a5acd8d57d9..cad68c4aac4 100644
--- a/prototype/sky/clouds/service_catalog/aws_catalog.py
+++ b/prototype/sky/clouds/service_catalog/aws_catalog.py
@@ -14,9 +14,11 @@
 
 
 def get_hourly_cost(instance_type: str,
-                    region: str = _DEFAULT_REGION,
+                    region: Optional[str] = None,
                     use_spot: bool = False) -> float:
     """Returns the cost, or the cheapest cost among all zones for spot."""
+    if region is None:
+        region = _DEFAULT_REGION
     return common.get_hourly_cost_impl(_df, instance_type, region, use_spot)
 
 
diff --git a/prototype/sky/clouds/service_catalog/azure_catalog.py b/prototype/sky/clouds/service_catalog/azure_catalog.py
index dc0858be342..c7a73b08b6a 100644
--- a/prototype/sky/clouds/service_catalog/azure_catalog.py
+++ b/prototype/sky/clouds/service_catalog/azure_catalog.py
@@ -15,10 +15,12 @@
 
 
 def get_hourly_cost(instance_type: str,
-                    region: str = _DEFAULT_REGION,
+                    region: Optional[str] = None,
                     use_spot: bool = False) -> float:
     """Returns the cost, or the cheapest cost among all zones for spot."""
     # Ref: https://azure.microsoft.com/en-us/support/legal/offer-details/
+    if region is None:
+        region = _DEFAULT_REGION
     assert not use_spot, 'Current Azure subscription does not support spot.'
     return common.get_hourly_cost_impl(_df, instance_type, region, use_spot)
 
diff --git a/prototype/sky/clouds/service_catalog/gcp_catalog.py b/prototype/sky/clouds/service_catalog/gcp_catalog.py
index d472ffcfacc..8df6db3c6b6 100644
--- a/prototype/sky/clouds/service_catalog/gcp_catalog.py
+++ b/prototype/sky/clouds/service_catalog/gcp_catalog.py
@@ -31,9 +31,11 @@ def _get_accelerator(
 
 def get_accelerator_hourly_cost(accelerator: str,
                                 count: int,
-                                region: str = _DEFAULT_REGION,
+                                region: Optional[str] = None,
                                 use_spot: bool = False) -> float:
     """Returns the cost, or the cheapest cost among all zones for spot."""
+    if region is None:
+        region = _DEFAULT_REGION
     df = _get_accelerator(_df, accelerator, count, region)
     assert len(set(df['Price'])) == 1, df
     if not use_spot:
@@ -48,7 +50,7 @@ def get_accelerator_hourly_cost(accelerator: str,
 
 
 def list_accelerators(
-    gpus_only: bool = False,
+    gpus_only: bool,
     name_filter: Optional[str] = None,
 ) -> Dict[str, List[common.InstanceTypeInfo]]:
     """Returns all instance types in GCP offering GPUs."""
diff --git a/prototype/sky/dag.py b/prototype/sky/dag.py
index c608a400507..30958316e07 100644
--- a/prototype/sky/dag.py
+++ b/prototype/sky/dag.py
@@ -1,5 +1,4 @@
 """DAGs: user applications to be run on Sky."""
-import networkx as nx
 import pprint
 
 
@@ -39,6 +38,8 @@ class Dag:
 
     def __init__(self):
         self.tasks = []
+        import networkx as nx
+
         self.graph = nx.DiGraph()
 
     def add(self, task):
diff --git a/prototype/sky/data/data_transfer.py b/prototype/sky/data/data_transfer.py
index 8049dea6afa..7061fc70522 100644
--- a/prototype/sky/data/data_transfer.py
+++ b/prototype/sky/data/data_transfer.py
@@ -20,8 +20,6 @@
 import os
 from typing import Any
 
-from oauth2client.client import GoogleCredentials
-
 from sky import sky_logging
 from sky.cloud_adaptors import aws, gcp
 
@@ -39,6 +37,8 @@ def s3_to_gcs(s3_bucket_name: str, gs_bucket_name: str) -> None:
       s3_bucket_name: str; Name of the Amazon S3 Bucket
       gs_bucket_name: str; Name of the Google Cloud Storage Bucket
     """
+    from oauth2client.client import GoogleCredentials
+
     credentials = GoogleCredentials.get_application_default()
     storagetransfer = gcp.build('storagetransfer',
                                 'v1',
diff --git a/prototype/sky/docker_adaptor.py b/prototype/sky/docker_adaptor.py
new file mode 100644
index 00000000000..778e035196b
--- /dev/null
+++ b/prototype/sky/docker_adaptor.py
@@ -0,0 +1,23 @@
+"""Docker adaptors"""
+
+# pylint: disable=import-outside-toplevel
+
+
+def from_env():
+    import docker
+    return docker.from_env()
+
+
+def build_error():
+    import docker
+    return docker.errors.BuildError
+
+
+def not_found_error():
+    import docker
+    return docker.errors.NotFound
+
+
+def api_error():
+    import docker
+    return docker.errors.APIError
diff --git a/prototype/sky/optimizer.py b/prototype/sky/optimizer.py
index b83429159ce..be4c77d850c 100644
--- a/prototype/sky/optimizer.py
+++ b/prototype/sky/optimizer.py
@@ -4,7 +4,6 @@
 import pprint
 from typing import Dict, List, Optional
 
-import networkx as nx
 import numpy as np
 import tabulate
 
@@ -168,6 +167,7 @@ def _optimize_cost(
         minimize_cost: bool = True,
         blocked_launchable_resources: Optional[List[Resources]] = None,
     ):
+        import networkx as nx
         # TODO: The output of this function is useful. Should generate a
         # text plan and print to both console and a log file.
         graph = dag.get_graph()


Code review comments:
diff_hunk: @@ -0,0 +1,23 @@
+"""Docker adaptors"""
+
+# pylint: disable=import-outside-toplevel
+
+
+def from_env():
+    import docker
path: prototype/sky/docker_adaptor.py
text: @user1:
Consider https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Import_Statement_Overhead
```
docker = None

def f():
    global docker
    if docker is None:
        import docker
    ...
```

diff_hunk: @@ -1,27 +1,57 @@
 """Service catalog."""
 import collections
-from typing import Dict, List, Optional
+import importlib
+from typing import Dict, List, Optional, Union, TYPE_CHECKING
 
-from sky.clouds.service_catalog import aws_catalog
-from sky.clouds.service_catalog import azure_catalog
-from sky.clouds.service_catalog import common
-from sky.clouds.service_catalog import gcp_catalog
+if TYPE_CHECKING:
+    from sky.clouds import cloud
+    from sky.clouds.service_catalog import common
+
+CloudFilter = Optional[Union[List[str], str]]
+ALL_CLOUDS = ('aws', 'azure', 'gcp')
path: prototype/sky/clouds/service_catalog/__init__.py
text: @user1:
nit: _ALL_CLOUDS?

diff_hunk: @@ -1,27 +1,57 @@
 """Service catalog."""
 import collections
-from typing import Dict, List, Optional
+import importlib
+from typing import Dict, List, Optional, Union, TYPE_CHECKING
 
-from sky.clouds.service_catalog import aws_catalog
-from sky.clouds.service_catalog import azure_catalog
-from sky.clouds.service_catalog import common
-from sky.clouds.service_catalog import gcp_catalog
+if TYPE_CHECKING:
+    from sky.clouds import cloud
+    from sky.clouds.service_catalog import common
+
+CloudFilter = Optional[Union[List[str], str]]
+ALL_CLOUDS = ('aws', 'azure', 'gcp')
+
+
+def _map_clouds_catalog(clouds: CloudFilter, method_name, *args, **kwargs):
+    if clouds is None:
+        clouds = list(ALL_CLOUDS)
+    single = isinstance(clouds, str)
+    if single:
+        clouds = [clouds]
+
+    results = []
+    for cloud in clouds:
+        try:
+            cloud_module = importlib.import_module(
+                f'sky.clouds.service_catalog.{cloud}_catalog')
+        except ModuleNotFoundError:
+            raise ValueError(
+                'Cannot find module "sky.clouds.service_catalog'
+                f'.{cloud}_catalog" for cloud "{cloud}".') from None
+        try:
+            method = getattr(cloud_module, method_name)
+        except AttributeError:
+            raise AttributeError(f'Cloud "{cloud}" does not implement the '
path: prototype/sky/clouds/service_catalog/__init__.py
text: @user1:
nit: Cloud "{cloud}" does not -> Module {cloud}_catalog does not