Repository: skypilot-org/skypilot
Pull request: [core] Lazy import modules to reduce importing time
<!-- Describe the changes in this PR -->
Part of #4678 

Improvements for the core operations:
- sky exec, mean time for 5 runs from 3.45s to 2.747s, about 20% faster
- sky launch on existing cluster, mean time for 5 runs from 14.901s to 13.08s, about 12% faster
- sky status, mean time for 5 runs from 2.426s to 2.157s, about 11% faster
- sky queue, mean time for 5 runs from 3.155s to 2.734s, about 13% faster

This PR only covers the third-party modules to import lazily, however, from the test result below, it still takes a long time to import the whole sky module.
So this needs to be improved further, e.g.:
- Lazy import of other unnecessary modules
- Split some core modules further to make the import more fine-grained


<!-- Describe the tests ran -->
<!-- Unit tests (tests/test_*.py) are part of GitHub CI; below are tests that launch on the cloud. -->

Tested (run the relevant ones):

- [x] Code formatting: `bash format.sh`
- [x] Any manual or new tests for this PR (please check the separate comments below)
- [ ] All smoke tests: `pytest tests/test_smoke.py` 
- [ ] Relevant individual smoke tests: `pytest tests/test_smoke.py::test_fill_in_the_name` 
- [ ] Backward compatibility tests: `conda deactivate; bash -i tests/backward_compatibility_tests.sh`


Problem statement:
`import sky` almost imports all subpackages and adds 1s overhead to all operations
Found that our backend module is adding a significant startup cost to all operations. Even importing just `sky.utils.common_utils` takes ~0.95s, which is almost the same as importing all of `sky` (~1.04s)!

```bash
# Importing all of sky
$ time python -c "import sky"
real    0m1.036s

# Just importing a utility module
$ time python -c "from sky.utils import common_utils"
real    0m0.954s
```

This means any small script or operation that uses sky modules gets a ~1s tax just from imports. We should look into:
- Lazy loading non-essential parts of the backend
- Breaking up the monolithic backend module
- Moving some initializations to happen on-demand

Cc'ing @cblmemo

Hints from issue discussion:
I suspect this is in large part due to significant I/O that we do on module load, like setting up databases.
> I suspect this is in large part due to significant I/O that we do on module load, like setting up databases.

Can we make it lazy?
After further investigation, I found that the root cause is our `__init__.py` eagerly importing almost everything. Even for a simple module like `env_options.py`:
```python
"""Global environment options for sky."""
import enum
import os
from typing import Dict

class Options(enum.Enum):
    ...
```

Importing it results in:
```console
$ time python -c "from sky.utils import env_options"

________________________________________________________
Executed in    1.68 secs    fish           external
   usr time    1.68 secs    0.00 micros    1.68 secs
   sys time    0.19 secs  930.00 micros    0.19 secs
```

This happens because when executing `from sky.utils import env_options`, Python first loads the entire sky module. Due to the heavy import graph in `__init__.py`, it ends up pulling in backends, clouds, and almost every other submodule.

Since `sys.modules` is process-wide, whenever we use the command runner to perform an operation, it launches a new process and reloads the entire sky module. This leads to significant overhead and wasted resources, especially for small, isolated operations.

We should refactor `__init__.py` to avoid eager imports and only expose critical APIs, implement lazy loading for non-essential modules and thus defer costly initializations to runtime (on-demand).

Cc’ing @cblmemo for further thoughts.
Additionally, using the following command helps visualize the import time breakdown:
```console
python -X importtime -c "import sky"
```
This clearly shows which submodules are contributing the most to the import overhead.

| Module | Time (us) | Percentage |
|-------------------------|-----------|------------|
| `sky.clouds` | 630,053 | 61.0% |
| `sky.backends` | 206,485 | 20.0% |
| `sky.jobs` | 67,881 | 6.6% |
| `sky.authentication` | 73,163 | 7.1% |
| `sky.data` | 13,703 | 1.3% |
| Other modules | 41,696 | 4.0% |
| Total | 1,032,981 | 100% |

We can take inspiration from ray-project/ray#30219, where they made expensive subpackage imports lazy, and significantly reduced the startup overhead without affecting functionality.
Good catch @andylizf! We have applied lazy imports for adaptors but we can see if we can do something with the cloud modules as well.
Thanks for the quick feedback @Michaelvll! Do you think the lazy import approach from https://github.com/ray-project/ray/pull/30219 would be reasonable for `sky.clouds`?
Yep, I think that is valid. We should test if that can actually help our `import` time. : )

Btw, do we know waht is the most expensive `cloud` that causes the long importing time for `sky.clouds`?
I also wonder if `sky.__commit__` is adding significant overhead. It calls `git` CLI twice - it only takes ~10ms on Linux but I seem to remember it can be quite slow on macos
IIRC, correct me if I am wrong, `sky.__commit__` should only be set by calling `git` during `pip install` 
I think that's true for install from pypi, but not for dev, which is presumably what Andy's testing.

Commit message: remove autouse for the fixture

Patch to review:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e227419c127..397f9f6e1a4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,18 +3,19 @@
 Thank you for your interest in contributing to SkyPilot! We welcome and value
 all contributions to the project, including but not limited to:
 
-* [Bug reports](https://github.com/skypilot-org/skypilot/issues) and [discussions](https://github.com/skypilot-org/skypilot/discussions)
-* [Pull requests](https://github.com/skypilot-org/skypilot/pulls) for bug fixes and new features
-* Test cases to make the codebase more robust
-* Examples
-* Documentation
-* Tutorials, blog posts and talks on SkyPilot
+- [Bug reports](https://github.com/skypilot-org/skypilot/issues) and [discussions](https://github.com/skypilot-org/skypilot/discussions)
+- [Pull requests](https://github.com/skypilot-org/skypilot/pulls) for bug fixes and new features
+- Test cases to make the codebase more robust
+- Examples
+- Documentation
+- Tutorials, blog posts and talks on SkyPilot
 
 ## Contributing code
 
 We use GitHub to track issues and features. For new contributors, we recommend looking at issues labeled ["good first issue"](https://github.com/sky-proj/sky/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22+).
 
 ### Installing SkyPilot for development
+
 ```bash
 # SkyPilot requires python >= 3.7.
 # You can just install the dependencies for
@@ -24,7 +25,9 @@ pip install -r requirements-dev.txt
 ```
 
 ### Testing
+
 To run smoke tests (NOTE: Running all smoke tests launches ~20 clusters):
+
 ```
 # Run all tests on AWS and Azure (default smoke test clouds)
 pytest tests/test_smoke.py
@@ -49,6 +52,7 @@ pytest tests/test_smoke.py --generic-cloud azure
 ```
 
 For profiling code, use:
+
 ```
 pip install py-spy # py-spy is a sampling profiler for Python programs
 py-spy record -t -o sky.svg -- python -m sky.cli status # Or some other command
@@ -57,6 +61,7 @@ py-spy -h # For more options
 ```
 
 #### Testing in a container
+
 It is often useful to test your changes in a clean environment set up from scratch. Using a container is a good way to do this.
 We have a dev container image `berkeleyskypilot/skypilot-debug` which we use for debugging skypilot inside a container. Use this image by running:
 
@@ -67,14 +72,16 @@ docker run --platform linux/amd64 -it --rm --name skypilot-debug berkeleyskypilo
 ```
 
 It has some convenience features which you might find helpful (see [Dockerfile](https://github.com/skypilot-org/skypilot/blob/dev/dockerfile_debug/Dockerfile_debug)):
-* Common dependencies and some utilities (rsync, screen, vim, nano etc) are pre-installed
-* requirements-dev.txt is pre-installed
-* Environment variables for dev/debug are set correctly
-* Automatically clones the latest master to `/sky_repo/skypilot` when the container is launched.
-  * Note that you still have to manually run `pip install -e ".[all]"` to install skypilot, it is not pre-installed.
-  * If your branch is on the SkyPilot repo, you can run `git checkout <your_branch>` to switch to your branch.
+
+- Common dependencies and some utilities (rsync, screen, vim, nano etc) are pre-installed
+- requirements-dev.txt is pre-installed
+- Environment variables for dev/debug are set correctly
+- Automatically clones the latest master to `/sky_repo/skypilot` when the container is launched.
+  - Note that you still have to manually run `pip install -e ".[all]"` to install skypilot, it is not pre-installed.
+  - If your branch is on the SkyPilot repo, you can run `git checkout <your_branch>` to switch to your branch.
 
 ### Submitting pull requests
+
 - Fork the SkyPilot repository and create a new branch for your changes.
 - If relevant, add tests for your changes. For changes that touch the core system, run the [smoke tests](#testing) and ensure they pass.
 - Follow the [Google style guide](https://google.github.io/styleguide/pyguide.html).
@@ -87,16 +94,27 @@ It has some convenience features which you might find helpful (see [Dockerfile](
 
 These are suggestions, not strict rules to follow. When in doubt, follow the [style guide](https://google.github.io/styleguide/pyguide.html).
 
-* Use `TODO(author_name)`/`FIXME(author_name)` instead of blank `TODO/FIXME`. This is critical for tracking down issues. You can write TODOs with your name and assign it to others (on github) if it is someone else's issue.
-* Delete your branch after merging it. This keeps the repo clean and faster to sync.
-* Use an exception if this is an error. Only use `assert` for debugging or proof-checking purposes. This is because exception messages usually contain more information.
-* Use modern python features and styles that increases code quality.
-  * Use f-string instead of `.format()` for short expressions to increase readability.
-  * Use `class MyClass:` instead of `class MyClass(object):`. The later one was a workaround for python2.x.
-  * Use `abc` module for abstract classes to ensure all abstract methods are implemented.
-  * Use python typing. But you should not import external objects just for typing. Instead, import typing-only external objects under `if typing.TYPE_CHECKING:`.
+- Use `TODO(author_name)`/`FIXME(author_name)` instead of blank `TODO/FIXME`. This is critical for tracking down issues. You can write TODOs with your name and assign it to others (on github) if it is someone else's issue.
+- Delete your branch after merging it. This keeps the repo clean and faster to sync.
+- Use an exception if this is an error. Only use `assert` for debugging or proof-checking purposes. This is because exception messages usually contain more information.
+- Use [`LazyImport`](https://github.com/skypilot-org/skypilot/blob/master/sky/adaptors/common.py) for third-party modules that meet these criteria:
+  - The module is imported during `import sky`
+  - The module has a significant import time (e.g. > 100ms)
+- To measure import time:
+  - Basic check: `python -X importtime -c "import sky"`
+  - Detailed visualization: use [`tuna`](https://github.com/nschloe/tuna) to analyze import time:
+    ```bash
+    python -X importtime -c "import sky" 2> import.log
+    tuna import.log
+    ```
+- Use modern python features and styles that increases code quality.
+  - Use f-string instead of `.format()` for short expressions to increase readability.
+  - Use `class MyClass:` instead of `class MyClass(object):`. The later one was a workaround for python2.x.
+  - Use `abc` module for abstract classes to ensure all abstract methods are implemented.
+  - Use python typing. But you should not import external objects just for typing. Instead, import typing-only external objects under `if typing.TYPE_CHECKING:`.
 
 ### Environment variables for developers
+
 - `export SKYPILOT_DISABLE_USAGE_COLLECTION=1` to disable usage logging.
 - `export SKYPILOT_DEBUG=1` to show debugging logs (use logging.DEBUG level).
 - `export SKYPILOT_MINIMIZE_LOGGING=1` to minimize logging. Useful when trying to avoid multiple lines of output, such as for demos.
diff --git a/sky/adaptors/common.py b/sky/adaptors/common.py
index d039813cd32..89c08f9c495 100644
--- a/sky/adaptors/common.py
+++ b/sky/adaptors/common.py
@@ -6,15 +6,19 @@
 
 
 class LazyImport:
-    """Lazy importer for heavy modules or cloud modules only when enabled.
+    """Lazy importer for modules.
 
-    We use this for pandas and networkx, as they can be time-consuming to import
-    (0.1-0.2 seconds). With this class, we can avoid the unnecessary import time
-    when the module is not used (e.g., `networkx` should not be imported for
-    `sky status and `pandas` should not be imported for `sky exec`).
+    This is mainly used in two cases:
+    1. Heavy 3rd party modules: They can be time-consuming to import
+    and not necessary for all `sky` imports, e.g., numpy(700+ms),
+    pendulum(500+ms), cryptography(500+ms), pandas(200+ms), and
+    networkx(100+ms), etc. With this class, we can avoid the
+    unnecessary import time when the module is not used (e.g.,
+    `networkx` should not be imported for `sky status` and `pandas`
+    should not be imported for `sky exec`).
 
-    We also use this for cloud adaptors, because we do not want to import the
-    cloud dependencies when it is not enabled.
+    2. Cloud modules in cloud adaptors: cloud dependencies are only required
+    when a cloud is enabled, so we only import them when actually needed.
     """
 
     def __init__(self,
diff --git a/sky/adaptors/ibm.py b/sky/adaptors/ibm.py
index 02d581063a5..5dbddd0ddaf 100644
--- a/sky/adaptors/ibm.py
+++ b/sky/adaptors/ibm.py
@@ -6,9 +6,6 @@
 import multiprocessing
 import os
 
-import requests
-import yaml
-
 from sky import sky_logging
 from sky.adaptors import common
 
@@ -27,6 +24,9 @@
                               import_error_message=_IMPORT_ERROR_MESSAGE)
 ibm_botocore = common.LazyImport('ibm_botocore',
                                  import_error_message=_IMPORT_ERROR_MESSAGE)
+requests = common.LazyImport('requests',
+                             import_error_message=_IMPORT_ERROR_MESSAGE)
+yaml = common.LazyImport('yaml', import_error_message=_IMPORT_ERROR_MESSAGE)
 
 
 def read_credential_file():
diff --git a/sky/authentication.py b/sky/authentication.py
index eb5ce8001e9..6c852778a56 100644
--- a/sky/authentication.py
+++ b/sky/authentication.py
@@ -25,19 +25,17 @@
 import socket
 import subprocess
 import sys
+import typing
 from typing import Any, Dict, Tuple
 import uuid
 
 import colorama
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import serialization
-from cryptography.hazmat.primitives.asymmetric import rsa
 import filelock
-import yaml
 
 from sky import clouds
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.adaptors import gcp
 from sky.adaptors import ibm
 from sky.adaptors import kubernetes
@@ -67,6 +65,11 @@
 # the former dir is empheral.
 _SSH_KEY_PATH_PREFIX = '~/.sky/clients/{user_hash}/ssh'
 
+if typing.TYPE_CHECKING:
+    import yaml
+else:
+    yaml = adaptors_common.LazyImport('yaml')
+
 
 def get_ssh_key_and_lock_path() -> Tuple[str, str, str]:
     user_hash = common_utils.get_user_hash()
@@ -82,6 +85,13 @@ def get_ssh_key_and_lock_path() -> Tuple[str, str, str]:
 
 
 def _generate_rsa_key_pair() -> Tuple[str, str]:
+    # Keep the import of the cryptography local to avoid expensive
+    # third-party imports when not needed.
+    # pylint: disable=import-outside-toplevel
+    from cryptography.hazmat.backends import default_backend
+    from cryptography.hazmat.primitives import serialization
+    from cryptography.hazmat.primitives.asymmetric import rsa
+
     key = rsa.generate_private_key(backend=default_backend(),
                                    public_exponent=65537,
                                    key_size=2048)
diff --git a/sky/backends/backend_utils.py b/sky/backends/backend_utils.py
index 900997dd824..1e642a35fd3 100644
--- a/sky/backends/backend_utils.py
+++ b/sky/backends/backend_utils.py
@@ -19,12 +19,7 @@
 import colorama
 import filelock
 from packaging import version
-import requests
-from requests import adapters
-from requests.packages.urllib3.util import retry as retry_lib
-import rich.progress as rich_progress
 from typing_extensions import Literal
-import yaml
 
 import sky
 from sky import authentication as auth
@@ -36,6 +31,7 @@
 from sky import provision as provision_lib
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.provision import instance_setup
 from sky.provision.kubernetes import utils as kubernetes_utils
 from sky.skylet import constants
@@ -56,10 +52,23 @@
 from sky.utils import ux_utils
 
 if typing.TYPE_CHECKING:
+    import requests
+    from requests import adapters
+    from requests.packages.urllib3.util import retry as retry_lib
+    import rich.progress as rich_progress
+    import yaml
+
     from sky import resources as resources_lib
     from sky import task as task_lib
     from sky.backends import cloud_vm_ray_backend
     from sky.backends import local_docker_backend
+else:
+    yaml = adaptors_common.LazyImport('yaml')
+    requests = adaptors_common.LazyImport('requests')
+    rich_progress = adaptors_common.LazyImport('rich.progress')
+    adapters = adaptors_common.LazyImport('requests.adapters')
+    retry_lib = adaptors_common.LazyImport(
+        'requests.packages.urllib3.util.retry')
 
 logger = sky_logging.init_logger(__name__)
 
diff --git a/sky/client/common.py b/sky/client/common.py
index e4697da05e1..900879ce39e 100644
--- a/sky/client/common.py
+++ b/sky/client/common.py
@@ -14,10 +14,8 @@
 import uuid
 import zipfile
 
-import httpx
-import requests
-
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.data import data_utils
 from sky.data import storage_utils
 from sky.server import common as server_common
@@ -29,8 +27,14 @@
 from sky.utils import ux_utils
 
 if typing.TYPE_CHECKING:
+    import httpx
+    import requests
+
     import sky
     import sky.dag as dag_lib
+else:
+    httpx = adaptors_common.LazyImport('httpx')
+    requests = adaptors_common.LazyImport('requests')
 
 logger = sky_logging.init_logger(__name__)
 
@@ -143,7 +147,7 @@ def __iter__(self):
 
 @dataclasses.dataclass
 class UploadChunkParams:
-    client: httpx.Client
+    client: 'httpx.Client'
     upload_id: str
     chunk_index: int
     total_chunks: int
diff --git a/sky/client/sdk.py b/sky/client/sdk.py
index 86b93c4b285..5505ef4820d 100644
--- a/sky/client/sdk.py
+++ b/sky/client/sdk.py
@@ -22,14 +22,13 @@
 import click
 import colorama
 import filelock
-import psutil
-import requests
 
 from sky import admin_policy
 from sky import backends
 from sky import exceptions
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.client import common as client_common
 from sky.server import common as server_common
 from sky.server.requests import payloads
@@ -50,14 +49,20 @@
 if typing.TYPE_CHECKING:
     import io
 
+    import psutil
+    import requests
+
     import sky
+else:
+    psutil = adaptors_common.LazyImport('psutil')
+    requests = adaptors_common.LazyImport('requests')
 
 logger = sky_logging.init_logger(__name__)
 logging.getLogger('httpx').setLevel(logging.CRITICAL)
 
 
 def stream_response(request_id: Optional[str],
-                    response: requests.Response,
+                    response: 'requests.Response',
                     output_stream: Optional['io.TextIOBase'] = None) -> Any:
     """Streams the response to the console.
 
diff --git a/sky/clouds/fluidstack.py b/sky/clouds/fluidstack.py
index ec52cf85d31..a0d12c1a729 100644
--- a/sky/clouds/fluidstack.py
+++ b/sky/clouds/fluidstack.py
@@ -3,9 +3,8 @@
 import typing
 from typing import Dict, Iterator, List, Optional, Tuple, Union
 
-import requests
-
 from sky import clouds
+from sky.adaptors import common as adaptors_common
 from sky.clouds import service_catalog
 from sky.provision.fluidstack import fluidstack_utils
 from sky.utils import registry
@@ -18,8 +17,12 @@
     fluidstack_utils.FLUIDSTACK_API_KEY_PATH
 ]
 if typing.TYPE_CHECKING:
+    import requests
+
     # Renaming to avoid shadowing variables.
     from sky import resources as resources_lib
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 
 @registry.CLOUD_REGISTRY.register
diff --git a/sky/clouds/lambda_cloud.py b/sky/clouds/lambda_cloud.py
index a24774e2146..201864f5cdf 100644
--- a/sky/clouds/lambda_cloud.py
+++ b/sky/clouds/lambda_cloud.py
@@ -2,9 +2,8 @@
 import typing
 from typing import Dict, Iterator, List, Optional, Tuple, Union
 
-import requests
-
 from sky import clouds
+from sky.adaptors import common as adaptors_common
 from sky.clouds import service_catalog
 from sky.provision.lambda_cloud import lambda_utils
 from sky.utils import registry
@@ -12,8 +11,12 @@
 from sky.utils import status_lib
 
 if typing.TYPE_CHECKING:
+    import requests
+
     # Renaming to avoid shadowing variables.
     from sky import resources as resources_lib
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 # Minimum set of files under ~/.lambda_cloud that grant Lambda Cloud access.
 _CREDENTIAL_FILES = [
diff --git a/sky/clouds/paperspace.py b/sky/clouds/paperspace.py
index cd8a32fafe8..379302bf7bd 100644
--- a/sky/clouds/paperspace.py
+++ b/sky/clouds/paperspace.py
@@ -3,16 +3,19 @@
 import typing
 from typing import Dict, Iterator, List, Optional, Tuple, Union
 
-import requests
-
 from sky import clouds
+from sky.adaptors import common as adaptors_common
 from sky.clouds import service_catalog
 from sky.provision.paperspace import utils
 from sky.utils import registry
 from sky.utils import resources_utils
 
 if typing.TYPE_CHECKING:
+    import requests
+
     from sky import resources as resources_lib
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 _CREDENTIAL_FILES = [
     # credential files for Paperspace,
diff --git a/sky/clouds/service_catalog/common.py b/sky/clouds/service_catalog/common.py
index dee92979118..e0b6035868d 100644
--- a/sky/clouds/service_catalog/common.py
+++ b/sky/clouds/service_catalog/common.py
@@ -8,7 +8,6 @@
 from typing import Callable, Dict, List, NamedTuple, Optional, Tuple, Union
 
 import filelock
-import requests
 
 from sky import sky_logging
 from sky.adaptors import common as adaptors_common
@@ -21,8 +20,10 @@
 
 if typing.TYPE_CHECKING:
     import pandas as pd
+    import requests
 else:
     pd = adaptors_common.LazyImport('pandas')
+    requests = adaptors_common.LazyImport('requests')
 
 logger = sky_logging.init_logger(__name__)
 
diff --git a/sky/clouds/utils/scp_utils.py b/sky/clouds/utils/scp_utils.py
index 4efc79313c5..5166c8ef1cb 100644
--- a/sky/clouds/utils/scp_utils.py
+++ b/sky/clouds/utils/scp_utils.py
@@ -11,10 +11,16 @@
 import logging
 import os
 import time
+import typing
 from typing import Any, Dict, List, Optional
 from urllib import parse
 
-import requests
+from sky.adaptors import common as adaptors_common
+
+if typing.TYPE_CHECKING:
+    import requests
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 CREDENTIALS_PATH = '~/.scp/scp_credential'
 API_ENDPOINT = 'https://openapi.samsungsdscloud.com'
@@ -98,7 +104,7 @@ def keys(self):
             return list(metadata.keys())
 
 
-def raise_scp_error(response: requests.Response) -> None:
+def raise_scp_error(response: 'requests.Response') -> None:
     """Raise SCPCloudError if appropriate. """
     status_code = response.status_code
     if status_code in (200, 202):
diff --git a/sky/clouds/vsphere.py b/sky/clouds/vsphere.py
index 243791cf578..ff530ae0e8a 100644
--- a/sky/clouds/vsphere.py
+++ b/sky/clouds/vsphere.py
@@ -3,9 +3,8 @@
 import typing
 from typing import Dict, Iterator, List, Optional, Tuple, Union
 
-import requests
-
 from sky import clouds
+from sky.adaptors import common as adaptors_common
 from sky.clouds import service_catalog
 from sky.provision.vsphere import vsphere_utils
 from sky.provision.vsphere.vsphere_utils import get_vsphere_credentials
@@ -15,8 +14,12 @@
 from sky.utils import resources_utils
 
 if typing.TYPE_CHECKING:
+    import requests
+
     # Renaming to avoid shadowing variables.
     from sky import resources as resources_lib
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 _CLOUD_VSPHERE = 'vsphere'
 _CREDENTIAL_FILES = [
diff --git a/sky/jobs/client/sdk.py b/sky/jobs/client/sdk.py
index 7c9b551bbac..bd7f2844214 100644
--- a/sky/jobs/client/sdk.py
+++ b/sky/jobs/client/sdk.py
@@ -5,9 +5,9 @@
 import webbrowser
 
 import click
-import requests
 
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.client import common as client_common
 from sky.client import sdk
 from sky.server import common as server_common
@@ -20,7 +20,11 @@
 if typing.TYPE_CHECKING:
     import io
 
+    import requests
+
     import sky
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 logger = sky_logging.init_logger(__name__)
 
diff --git a/sky/jobs/scheduler.py b/sky/jobs/scheduler.py
index 23a9e9a848e..b3544db89b7 100644
--- a/sky/jobs/scheduler.py
+++ b/sky/jobs/scheduler.py
@@ -41,17 +41,23 @@
 from functools import lru_cache
 import os
 import time
+import typing
 
 import filelock
-import psutil
 
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.jobs import constants as managed_job_constants
 from sky.jobs import state
 from sky.skylet import constants
 from sky.utils import common_utils
 from sky.utils import subprocess_utils
 
+if typing.TYPE_CHECKING:
+    import psutil
+else:
+    psutil = adaptors_common.LazyImport('psutil')
+
 logger = sky_logging.init_logger('sky.jobs.controller')
 
 # The _MANAGED_JOB_SCHEDULER_LOCK should be held whenever we are checking the
diff --git a/sky/jobs/utils.py b/sky/jobs/utils.py
index b73244bd4a8..bea0e3ab6b3 100644
--- a/sky/jobs/utils.py
+++ b/sky/jobs/utils.py
@@ -17,13 +17,13 @@
 
 import colorama
 import filelock
-import psutil
 from typing_extensions import Literal
 
 from sky import backends
 from sky import exceptions
 from sky import global_user_state
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.backends import backend_utils
 from sky.jobs import constants as managed_job_constants
 from sky.jobs import scheduler
@@ -40,8 +40,12 @@
 from sky.utils import ux_utils
 
 if typing.TYPE_CHECKING:
+    import psutil
+
     import sky
     from sky import dag as dag_lib
+else:
+    psutil = adaptors_common.LazyImport('psutil')
 
 logger = sky_logging.init_logger(__name__)
 
diff --git a/sky/optimizer.py b/sky/optimizer.py
index 360de187738..fb95e884c0f 100644
--- a/sky/optimizer.py
+++ b/sky/optimizer.py
@@ -6,7 +6,6 @@
 from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
 
 import colorama
-import numpy as np
 import prettytable
 
 from sky import check as sky_check
@@ -28,10 +27,12 @@
 
 if typing.TYPE_CHECKING:
     import networkx as nx
+    import numpy as np
 
     from sky import dag as dag_lib
 else:
     nx = adaptors_common.LazyImport('networkx')
+    np = adaptors_common.LazyImport('numpy')
 
 logger = sky_logging.init_logger(__name__)
 
diff --git a/sky/provision/fluidstack/fluidstack_utils.py b/sky/provision/fluidstack/fluidstack_utils.py
index 2ed18523696..a3d6fd84239 100644
--- a/sky/provision/fluidstack/fluidstack_utils.py
+++ b/sky/provision/fluidstack/fluidstack_utils.py
@@ -3,13 +3,18 @@
 import json
 import os
 import time
+import typing
 from typing import Any, Dict, List
 import uuid
 
-import requests
-
+from sky.adaptors import common as adaptors_common
 from sky.utils import annotations
 
+if typing.TYPE_CHECKING:
+    import requests
+else:
+    requests = adaptors_common.LazyImport('requests')
+
 
 def get_key_suffix():
     return str(uuid.uuid4()).replace('-', '')[:8]
diff --git a/sky/provision/kubernetes/config.py b/sky/provision/kubernetes/config.py
index 0fe920be9d6..d05a7b9ee56 100644
--- a/sky/provision/kubernetes/config.py
+++ b/sky/provision/kubernetes/config.py
@@ -3,16 +3,21 @@
 import logging
 import math
 import os
+import typing
 from typing import Any, Dict, Optional, Union
 
-import yaml
-
+from sky.adaptors import common as adaptors_common
 from sky.adaptors import kubernetes
 from sky.provision import common
 from sky.provision.kubernetes import network_utils
 from sky.provision.kubernetes import utils as kubernetes_utils
 from sky.utils import kubernetes_enums
 
+if typing.TYPE_CHECKING:
+    import yaml
+else:
+    yaml = adaptors_common.LazyImport('yaml')
+
 logger = logging.getLogger(__name__)
 
 # Timeout for deleting a Kubernetes resource (in seconds).
diff --git a/sky/provision/kubernetes/network_utils.py b/sky/provision/kubernetes/network_utils.py
index 828a1807c4d..9e11d0fe4ef 100644
--- a/sky/provision/kubernetes/network_utils.py
+++ b/sky/provision/kubernetes/network_utils.py
@@ -1,20 +1,26 @@
 """Kubernetes network provisioning utils."""
 import os
 import time
+import typing
 from typing import Dict, List, Optional, Tuple, Union
 
-import jinja2
-import yaml
-
 import sky
 from sky import exceptions
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.adaptors import kubernetes
 from sky.provision.kubernetes import utils as kubernetes_utils
 from sky.utils import kubernetes_enums
 from sky.utils import ux_utils
 
+if typing.TYPE_CHECKING:
+    import jinja2
+    import yaml
+else:
+    jinja2 = adaptors_common.LazyImport('jinja2')
+    yaml = adaptors_common.LazyImport('yaml')
+
 logger = sky_logging.init_logger(__name__)
 
 _INGRESS_TEMPLATE_NAME = 'kubernetes-ingress.yml.j2'
diff --git a/sky/provision/kubernetes/utils.py b/sky/provision/kubernetes/utils.py
index dcfe05607aa..33784ca15f5 100644
--- a/sky/provision/kubernetes/utils.py
+++ b/sky/provision/kubernetes/utils.py
@@ -12,15 +12,13 @@
 from typing import Any, Dict, List, Optional, Set, Tuple, Union
 from urllib.parse import urlparse
 
-import jinja2
-import yaml
-
 import sky
 from sky import clouds
 from sky import exceptions
 from sky import models
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.adaptors import gcp
 from sky.adaptors import kubernetes
 from sky.provision import constants as provision_constants
@@ -38,8 +36,14 @@
 from sky.utils import ux_utils
 
 if typing.TYPE_CHECKING:
+    import jinja2
+    import yaml
+
     from sky import backends
     from sky import resources as resources_lib
+else:
+    jinja2 = adaptors_common.LazyImport('jinja2')
+    yaml = adaptors_common.LazyImport('yaml')
 
 # TODO(romilb): Move constants to constants.py
 DEFAULT_NAMESPACE = 'default'
diff --git a/sky/provision/lambda_cloud/lambda_utils.py b/sky/provision/lambda_cloud/lambda_utils.py
index cfd8e02ad23..ca5d2124571 100644
--- a/sky/provision/lambda_cloud/lambda_utils.py
+++ b/sky/provision/lambda_cloud/lambda_utils.py
@@ -3,12 +3,17 @@
 import json
 import os
 import time
+import typing
 from typing import Any, Dict, List, Optional, Tuple
 
-import requests
-
+from sky.adaptors import common as adaptors_common
 from sky.utils import common_utils
 
+if typing.TYPE_CHECKING:
+    import requests
+else:
+    requests = adaptors_common.LazyImport('requests')
+
 CREDENTIALS_PATH = '~/.lambda_cloud/lambda_keys'
 API_ENDPOINT = 'https://cloud.lambdalabs.com/api/v1'
 INITIAL_BACKOFF_SECONDS = 10
@@ -76,7 +81,7 @@ def refresh(self, instance_ids: List[str]) -> None:
             json.dump(metadata, f)
 
 
-def raise_lambda_error(response: requests.Response) -> None:
+def raise_lambda_error(response: 'requests.Response') -> None:
     """Raise LambdaCloudError if appropriate."""
     status_code = response.status_code
     if status_code == 200:
diff --git a/sky/provision/paperspace/utils.py b/sky/provision/paperspace/utils.py
index d9eceefba19..765324213d6 100644
--- a/sky/provision/paperspace/utils.py
+++ b/sky/provision/paperspace/utils.py
@@ -3,14 +3,19 @@
 import json
 import os
 import time
+import typing
 from typing import Any, Dict, List, Optional, Union
 
-import requests
-
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 import sky.provision.paperspace.constants as constants
 from sky.utils import common_utils
 
+if typing.TYPE_CHECKING:
+    import requests
+else:
+    requests = adaptors_common.LazyImport('requests')
+
 logger = sky_logging.init_logger(__name__)
 
 CREDENTIALS_PATH = '~/.paperspace/config.json'
@@ -25,7 +30,7 @@ class PaperspaceCloudError(Exception):
     pass
 
 
-def raise_paperspace_api_error(response: requests.Response) -> None:
+def raise_paperspace_api_error(response: 'requests.Response') -> None:
     """Raise PaperspaceCloudError if appropriate."""
     status_code = response.status_code
     if status_code == 200:
diff --git a/sky/provision/vsphere/common/ssl_helper.py b/sky/provision/vsphere/common/ssl_helper.py
index cf044200924..aabcb2de811 100644
--- a/sky/provision/vsphere/common/ssl_helper.py
+++ b/sky/provision/vsphere/common/ssl_helper.py
@@ -1,9 +1,14 @@
 """SSL Helper
 """
-
 import ssl
+import typing
+
+from sky.adaptors import common as adaptors_common
 
-import requests
+if typing.TYPE_CHECKING:
+    import requests
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 
 def get_unverified_context():
diff --git a/sky/provision/vsphere/common/vapiconnect.py b/sky/provision/vsphere/common/vapiconnect.py
index d3e33d3dc00..9b6a30b4955 100644
--- a/sky/provision/vsphere/common/vapiconnect.py
+++ b/sky/provision/vsphere/common/vapiconnect.py
@@ -1,11 +1,18 @@
 """Vapi Connect
 """
 
-import requests
+import typing
+
 from urllib3.exceptions import InsecureRequestWarning
 
+from sky.adaptors import common as adaptors_common
 from sky.adaptors import vsphere as vsphere_adaptor
 
+if typing.TYPE_CHECKING:
+    import requests
+else:
+    requests = adaptors_common.LazyImport('requests')
+
 
 def get_jsonrpc_endpoint_url(host):
     # The URL for the stub requests are made against the /api HTTP endpoint
diff --git a/sky/provision/vsphere/vsphere_utils.py b/sky/provision/vsphere/vsphere_utils.py
index 51f284b0fc6..cdea0439451 100644
--- a/sky/provision/vsphere/vsphere_utils.py
+++ b/sky/provision/vsphere/vsphere_utils.py
@@ -3,12 +3,12 @@
 import http.cookies as http_cookies
 import os
 import ssl
+import typing
 from typing import Any, Dict, List, Optional
 
-import yaml
-
 from sky import exceptions
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.adaptors import vsphere as vsphere_adaptor
 from sky.clouds.service_catalog import vsphere_catalog
 from sky.clouds.service_catalog.common import get_catalog_path
@@ -33,6 +33,11 @@
 from sky.provision.vsphere.common.vim_utils import wait_for_tasks
 from sky.provision.vsphere.common.vim_utils import wait_internal_ip_ready
 
+if typing.TYPE_CHECKING:
+    import yaml
+else:
+    yaml = adaptors_common.LazyImport('yaml')
+
 logger = sky_logging.init_logger(__name__)
 
 CREDENTIALS_PATH = '~/.vsphere/credential.yaml'
diff --git a/sky/serve/client/sdk.py b/sky/serve/client/sdk.py
index 589814d5865..a471e88b72f 100644
--- a/sky/serve/client/sdk.py
+++ b/sky/serve/client/sdk.py
@@ -4,8 +4,8 @@
 from typing import List, Optional, Union
 
 import click
-import requests
 
+from sky.adaptors import common as adaptors_common
 from sky.client import common as client_common
 from sky.server import common as server_common
 from sky.server.requests import payloads
@@ -15,8 +15,12 @@
 if typing.TYPE_CHECKING:
     import io
 
+    import requests
+
     import sky
     from sky.serve import serve_utils
+else:
+    requests = adaptors_common.LazyImport('requests')
 
 
 @usage_lib.entrypoint
diff --git a/sky/serve/serve_utils.py b/sky/serve/serve_utils.py
index b5fd506d409..e3c8908f2bd 100644
--- a/sky/serve/serve_utils.py
+++ b/sky/serve/serve_utils.py
@@ -18,16 +18,16 @@
 
 import colorama
 import filelock
-import psutil
-import requests
 
 from sky import backends
 from sky import exceptions
 from sky import global_user_state
+from sky.adaptors import common as adaptors_common
 from sky.serve import constants
 from sky.serve import serve_state
 from sky.skylet import constants as skylet_constants
 from sky.skylet import job_lib
+from sky.utils import annotations
 from sky.utils import common_utils
 from sky.utils import log_utils
 from sky.utils import message_utils
@@ -37,12 +37,22 @@
 
 if typing.TYPE_CHECKING:
     import fastapi
+    import psutil
+    import requests
 
     from sky.serve import replica_managers
+else:
+    psutil = adaptors_common.LazyImport('psutil')
+    requests = adaptors_common.LazyImport('requests')
+
+
+@annotations.lru_cache(scope='request')
+def get_num_service_threshold():
+    """Get number of services threshold, calculating it only when needed."""
+    system_memory_gb = psutil.virtual_memory().total // (1024**3)
+    return system_memory_gb // constants.CONTROLLER_MEMORY_USAGE_GB
+
 
-_SYSTEM_MEMORY_GB = psutil.virtual_memory().total // (1024**3)
-NUM_SERVICE_THRESHOLD = (_SYSTEM_MEMORY_GB //
-                         constants.CONTROLLER_MEMORY_USAGE_GB)
 _CONTROLLER_URL = 'http://localhost:{CONTROLLER_PORT}'
 
 # NOTE(dev): We assume log paths are either in ~/sky_logs/... or ~/.sky/...
@@ -607,7 +617,7 @@ def wait_service_registration(service_name: str, job_id: int) -> str:
             lb_port = record['load_balancer_port']
             if lb_port is not None:
                 return message_utils.encode_payload(lb_port)
-        elif len(serve_state.get_services()) >= NUM_SERVICE_THRESHOLD:
+        elif len(serve_state.get_services()) >= get_num_service_threshold():
             with ux_utils.print_exception_no_traceback():
                 raise RuntimeError('Max number of services reached. '
                                    'To spin up more services, please '
diff --git a/sky/serve/service.py b/sky/serve/service.py
index 42b76ebfa9a..49079419789 100644
--- a/sky/serve/service.py
+++ b/sky/serve/service.py
@@ -147,7 +147,8 @@ def _start(service_name: str, tmp_task_yaml: str, job_id: int):
     # Already checked before submit to controller.
     assert task.service is not None, task
     service_spec = task.service
-    if len(serve_state.get_services()) >= serve_utils.NUM_SERVICE_THRESHOLD:
+    if (len(serve_state.get_services()) >=
+            serve_utils.get_num_service_threshold()):
         cleanup_storage(tmp_task_yaml)
         with ux_utils.print_exception_no_traceback():
             raise RuntimeError('Max number of services reached.')
diff --git a/sky/serve/service_spec.py b/sky/serve/service_spec.py
index 75e2d2de5d7..606ed88368b 100644
--- a/sky/serve/service_spec.py
+++ b/sky/serve/service_spec.py
@@ -2,11 +2,11 @@
 import json
 import os
 import textwrap
+import typing
 from typing import Any, Dict, List, Optional
 
-import yaml
-
 from sky import serve
+from sky.adaptors import common as adaptors_common
 from sky.serve import constants
 from sky.serve import load_balancing_policies as lb_policies
 from sky.serve import serve_utils
@@ -14,6 +14,11 @@
 from sky.utils import schemas
 from sky.utils import ux_utils
 
+if typing.TYPE_CHECKING:
+    import yaml
+else:
+    yaml = adaptors_common.LazyImport('yaml')
+
 
 class SkyServiceSpec:
     """SkyServe service specification."""
diff --git a/sky/server/common.py b/sky/server/common.py
index 84c4fe852c8..0799dda334f 100644
--- a/sky/server/common.py
+++ b/sky/server/common.py
@@ -15,12 +15,11 @@
 
 import colorama
 import filelock
-import pydantic
-import requests
 
 from sky import exceptions
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.data import data_utils
 from sky.server import constants as server_constants
 from sky.skylet import constants
@@ -31,7 +30,13 @@
 from sky.utils import ux_utils
 
 if typing.TYPE_CHECKING:
+    import pydantic
+    import requests
+
     from sky import dag as dag_lib
+else:
+    pydantic = adaptors_common.LazyImport('pydantic')
+    requests = adaptors_common.LazyImport('requests')
 
 DEFAULT_SERVER_URL = 'http://127.0.0.1:46580'
 AVAILBLE_LOCAL_API_SERVER_HOSTS = ['0.0.0.0', 'localhost', '127.0.0.1']
@@ -145,7 +150,7 @@ def get_api_server_status(endpoint: Optional[str] = None) -> ApiServerInfo:
     return ApiServerInfo(status=ApiServerStatus.UNHEALTHY, api_version=None)
 
 
-def handle_request_error(response: requests.Response) -> None:
+def handle_request_error(response: 'requests.Response') -> None:
     if response.status_code != 200:
         with ux_utils.print_exception_no_traceback():
             raise RuntimeError(
@@ -155,7 +160,7 @@ def handle_request_error(response: requests.Response) -> None:
                 f'{response.text}')
 
 
-def get_request_id(response: requests.Response) -> RequestId:
+def get_request_id(response: 'requests.Response') -> RequestId:
     handle_request_error(response)
     request_id = response.headers.get('X-Request-ID')
     if request_id is None:
@@ -398,7 +403,7 @@ def api_server_user_logs_dir_prefix(
     return API_SERVER_CLIENT_DIR / user_hash / 'sky_logs'
 
 
-def request_body_to_params(body: pydantic.BaseModel) -> Dict[str, Any]:
+def request_body_to_params(body: 'pydantic.BaseModel') -> Dict[str, Any]:
     return {
         k: v for k, v in body.model_dump(mode='json').items() if v is not None
     }
diff --git a/sky/server/requests/payloads.py b/sky/server/requests/payloads.py
index 40271d3569e..2b1a23632c5 100644
--- a/sky/server/requests/payloads.py
+++ b/sky/server/requests/payloads.py
@@ -8,14 +8,14 @@
 import getpass
 import json
 import os
+import typing
 from typing import Any, Dict, List, Optional, Tuple, Union
 
-import pydantic
-
 from sky import admin_policy
 from sky import serve
 from sky import sky_logging
 from sky import skypilot_config
+from sky.adaptors import common as adaptors_common
 from sky.server import common
 from sky.skylet import constants
 from sky.usage import constants as usage_constants
@@ -25,6 +25,11 @@
 from sky.utils import common_utils
 from sky.utils import registry
 
+if typing.TYPE_CHECKING:
+    import pydantic
+else:
+    pydantic = adaptors_common.LazyImport('pydantic')
+
 logger = sky_logging.init_logger(__name__)
 
 
diff --git a/sky/skylet/autostop_lib.py b/sky/skylet/autostop_lib.py
index fea6c30f626..e44f88e6dde 100644
--- a/sky/skylet/autostop_lib.py
+++ b/sky/skylet/autostop_lib.py
@@ -2,15 +2,20 @@
 import pickle
 import shlex
 import time
+import typing
 from typing import List, Optional
 
-import psutil
-
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.skylet import configs
 from sky.skylet import constants
 from sky.utils import message_utils
 
+if typing.TYPE_CHECKING:
+    import psutil
+else:
+    psutil = adaptors_common.LazyImport('psutil')
+
 logger = sky_logging.init_logger(__name__)
 
 _AUTOSTOP_CONFIG_KEY = 'autostop_config'
diff --git a/sky/skylet/job_lib.py b/sky/skylet/job_lib.py
index 5d4c77ede8b..d3c2e2ae47a 100644
--- a/sky/skylet/job_lib.py
+++ b/sky/skylet/job_lib.py
@@ -11,14 +11,15 @@
 import signal
 import sqlite3
 import time
+import typing
 from typing import Any, Dict, List, Optional, Sequence
 
 import colorama
 import filelock
-import psutil
 
 from sky import global_user_state
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.skylet import constants
 from sky.utils import common_utils
 from sky.utils import db_utils
@@ -26,6 +27,11 @@
 from sky.utils import message_utils
 from sky.utils import subprocess_utils
 
+if typing.TYPE_CHECKING:
+    import psutil
+else:
+    psutil = adaptors_common.LazyImport('psutil')
+
 logger = sky_logging.init_logger(__name__)
 
 _LINUX_NEW_LINE = '\n'
diff --git a/sky/skypilot_config.py b/sky/skypilot_config.py
index 7151194db06..477b316c93a 100644
--- a/sky/skypilot_config.py
+++ b/sky/skypilot_config.py
@@ -53,18 +53,23 @@
 import os
 import pprint
 import tempfile
+import typing
 from typing import Any, Dict, Iterator, Optional, Tuple
 
-import yaml
-
 from sky import exceptions
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.skylet import constants
 from sky.utils import common_utils
 from sky.utils import config_utils
 from sky.utils import schemas
 from sky.utils import ux_utils
 
+if typing.TYPE_CHECKING:
+    import yaml
+else:
+    yaml = adaptors_common.LazyImport('yaml')
+
 logger = sky_logging.init_logger(__name__)
 
 # The config path is discovered in this order:
diff --git a/sky/task.py b/sky/task.py
index 537ef113dc4..295c291a720 100644
--- a/sky/task.py
+++ b/sky/task.py
@@ -9,12 +9,12 @@
                     Union)
 
 import colorama
-import yaml
 
 import sky
 from sky import clouds
 from sky import exceptions
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 import sky.dag
 from sky.data import data_utils
 from sky.data import storage as storage_lib
@@ -26,7 +26,11 @@
 from sky.utils import ux_utils
 
 if typing.TYPE_CHECKING:
+    import yaml
+
     from sky import resources as resources_lib
+else:
+    yaml = adaptors_common.LazyImport('yaml')
 
 logger = sky_logging.init_logger(__name__)
 
diff --git a/sky/utils/common_utils.py b/sky/utils/common_utils.py
index 1988868e11b..5e745d322a1 100644
--- a/sky/utils/common_utils.py
+++ b/sky/utils/common_utils.py
@@ -13,22 +13,30 @@
 import socket
 import sys
 import time
+import typing
 from typing import Any, Callable, Dict, List, Optional, Union
 import uuid
 
-import jinja2
-import jsonschema
-import psutil
-import yaml
-
 from sky import exceptions
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.skylet import constants
 from sky.usage import constants as usage_constants
 from sky.utils import annotations
 from sky.utils import ux_utils
 from sky.utils import validator
 
+if typing.TYPE_CHECKING:
+    import jinja2
+    import jsonschema
+    import psutil
+    import yaml
+else:
+    jinja2 = adaptors_common.LazyImport('jinja2')
+    jsonschema = adaptors_common.LazyImport('jsonschema')
+    psutil = adaptors_common.LazyImport('psutil')
+    yaml = adaptors_common.LazyImport('yaml')
+
 _USER_HASH_FILE = os.path.expanduser('~/.sky/user_hash')
 USER_HASH_LENGTH = 8
 
@@ -598,7 +606,7 @@ def validate_schema(obj, schema, err_msg_prefix='', skip_none=True):
         obj = {k: v for k, v in obj.items() if v is not None}
     err_msg = None
     try:
-        validator.SchemaValidator(schema).validate(obj)
+        validator.get_schema_validator()(schema).validate(obj)
     except jsonschema.ValidationError as e:
         if e.validator == 'additionalProperties':
             if tuple(e.schema_path) == ('properties', 'envs',
diff --git a/sky/utils/log_utils.py b/sky/utils/log_utils.py
index 392d8533e63..9869eacba31 100644
--- a/sky/utils/log_utils.py
+++ b/sky/utils/log_utils.py
@@ -2,20 +2,26 @@
 import enum
 import time
 import types
+import typing
 from typing import Callable, Iterator, List, Optional, TextIO, Type
 
 import colorama
-# slow due to https://github.com/python-pendulum/pendulum/issues/808
-# FIXME(aylei): bump pendulum if it get fixed
-import pendulum
 import prettytable
 
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.utils import rich_utils
 from sky.utils import ux_utils
 
 logger = sky_logging.init_logger(__name__)
 
+if typing.TYPE_CHECKING:
+    # slow due to https://github.com/python-pendulum/pendulum/issues/808
+    # FIXME(aylei): bump pendulum if it get fixed
+    import pendulum
+else:
+    pendulum = adaptors_common.LazyImport('pendulum')
+
 
 class LineProcessor(object):
     """A processor for log lines."""
diff --git a/sky/utils/rich_console_utils.py b/sky/utils/rich_console_utils.py
new file mode 100644
index 00000000000..7bc6891ef85
--- /dev/null
+++ b/sky/utils/rich_console_utils.py
@@ -0,0 +1,21 @@
+"""Utility functions for rich console."""
+import typing
+
+from sky.adaptors import common as adaptors_common
+
+if typing.TYPE_CHECKING:
+    import rich.console as rich_console
+else:
+    rich_console = adaptors_common.LazyImport('rich.console')
+
+_console = None  # Lazy initialized console
+
+
+# Move global console to a function to avoid
+# importing rich console if not used
+def get_console():
+    """Get or create the rich console."""
+    global _console
+    if _console is None:
+        _console = rich_console.Console(soft_wrap=True)
+    return _console
diff --git a/sky/utils/rich_utils.py b/sky/utils/rich_utils.py
index 2f4dae30083..79d5b705f8b 100644
--- a/sky/utils/rich_utils.py
+++ b/sky/utils/rich_utils.py
@@ -6,15 +6,18 @@
 import typing
 from typing import Dict, Iterator, Optional, Tuple, Union
 
-import rich.console as rich_console
-
+from sky.adaptors import common as adaptors_common
 from sky.utils import annotations
 from sky.utils import message_utils
+from sky.utils import rich_console_utils
 
 if typing.TYPE_CHECKING:
     import requests
+    import rich.console as rich_console
+else:
+    requests = adaptors_common.LazyImport('requests')
+    rich_console = adaptors_common.LazyImport('rich.console')
 
-console = rich_console.Console(soft_wrap=True)
 _statuses: Dict[str, Optional[Union['EncodedStatus',
                                     'rich_console.Status']]] = {
                                         'server': None,
@@ -219,7 +222,7 @@ def client_status(msg: str) -> Union['rich_console.Status', _NoOpConsoleStatus]:
     if (threading.current_thread() is threading.main_thread() and
             not sky_logging.is_silent()):
         if _statuses['client'] is None:
-            _statuses['client'] = console.status(msg)
+            _statuses['client'] = rich_console_utils.get_console().status(msg)
         return _RevertibleStatus(msg, 'client')
     return _NoOpConsoleStatus()
 
diff --git a/sky/utils/subprocess_utils.py b/sky/utils/subprocess_utils.py
index f0bafd8d3af..992b5d59120 100644
--- a/sky/utils/subprocess_utils.py
+++ b/sky/utils/subprocess_utils.py
@@ -8,19 +8,25 @@
 import subprocess
 import threading
 import time
+import typing
 from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union
 
 import colorama
-import psutil
 
 from sky import exceptions
 from sky import sky_logging
+from sky.adaptors import common as adaptors_common
 from sky.skylet import constants
 from sky.skylet import log_lib
 from sky.utils import common_utils
 from sky.utils import timeline
 from sky.utils import ux_utils
 
+if typing.TYPE_CHECKING:
+    import psutil
+else:
+    psutil = adaptors_common.LazyImport('psutil')
+
 logger = sky_logging.init_logger(__name__)
 
 _fd_limit_warning_shown = False
diff --git a/sky/utils/ux_utils.py b/sky/utils/ux_utils.py
index c8718ba0bd9..bbf539178a5 100644
--- a/sky/utils/ux_utils.py
+++ b/sky/utils/ux_utils.py
@@ -8,17 +8,15 @@
 from typing import Callable, Optional, Union
 
 import colorama
-import rich.console as rich_console
 
 from sky import sky_logging
 from sky.skylet import constants
 from sky.utils import common_utils
+from sky.utils import rich_console_utils
 
 if typing.TYPE_CHECKING:
     import pathlib
 
-console = rich_console.Console()
-
 INDENT_SYMBOL = f'{colorama.Style.DIM}├── {colorama.Style.RESET_ALL}'
 INDENT_LAST_SYMBOL = f'{colorama.Style.DIM}└── {colorama.Style.RESET_ALL}'
 
@@ -40,7 +38,7 @@ def console_newline():
 
     Useful when catching exceptions inside console.status()
     """
-    console.print()
+    rich_console_utils.get_console().print()
 
 
 @contextlib.contextmanager
diff --git a/sky/utils/validator.py b/sky/utils/validator.py
index aabce3a4780..d26163ffc6c 100644
--- a/sky/utils/validator.py
+++ b/sky/utils/validator.py
@@ -4,7 +4,14 @@
 allow for case-insensitive enum matching since this is currently not supported
 by the JSON Schema specification.
 """
-import jsonschema
+import typing
+
+from sky.adaptors import common as adaptors_common
+
+if typing.TYPE_CHECKING:
+    import jsonschema
+else:
+    jsonschema = adaptors_common.LazyImport('jsonschema')
 
 
 def case_insensitive_enum(validator, enums, instance, schema):
@@ -14,6 +21,9 @@ def case_insensitive_enum(validator, enums, instance, schema):
             f'{instance!r} is not one of {enums!r}')
 
 
-SchemaValidator = jsonschema.validators.extend(
-    jsonschema.Draft7Validator,
-    validators={'case_insensitive_enum': case_insensitive_enum})
+# Move this to a function to delay initialization
+def get_schema_validator():
+    """Get the schema validator class, initializing it only when needed."""
+    return jsonschema.validators.extend(
+        jsonschema.Draft7Validator,
+        validators={'case_insensitive_enum': case_insensitive_enum})
diff --git a/tests/unit_tests/test_sky_import.py b/tests/unit_tests/test_sky_import.py
new file mode 100644
index 00000000000..73277a42119
--- /dev/null
+++ b/tests/unit_tests/test_sky_import.py
@@ -0,0 +1,55 @@
+"""Ensure the lazy import of modules is not reverted.
+
+This test ensures that the lazy import of modules is not reverted.
+"""
+import copy
+import gc
+import sys
+
+import pytest
+
+from sky.adaptors.common import LazyImport
+
+
+@pytest.fixture
+def lazy_import_modules():
+    """Get list of lazy-imported module names."""
+    return [
+        obj._module_name
+        for obj in gc.get_objects()
+        if isinstance(obj, LazyImport) and hasattr(obj, '_module_name')
+    ]
+
+
+@pytest.fixture
+def mock_delete_modules(request, monkeypatch):
+    """Fixture to mock delete modules."""
+    # Get the lazy_import_modules fixture
+    lazy_modules = request.getfixturevalue('lazy_import_modules')
+
+    # Create a copy of sys.modules for this test
+    test_modules = copy.copy(sys.modules)
+
+    # Clean modules in the copy
+    for module in lazy_modules:
+        if module in test_modules:
+            del test_modules[module]
+
+    # Use monkeypatch to temporarily replace sys.modules for this test only
+    monkeypatch.setattr(sys, 'modules', test_modules)
+
+    yield
+
+    # monkeypatch automatically restores sys.modules after the test
+
+
+def test_sky_import(lazy_import_modules, mock_delete_modules):
+    # Import sky
+    try:
+        import sky
+    except Exception as e:
+        print(f"Failed to import sky: {e}")
+        raise
+
+    for module in lazy_import_modules:
+        assert module not in sys.modules, f"Module {module} is not lazy imported"


Code review comments:
diff_hunk: @@ -0,0 +1,55 @@
+"""Ensure the lazy import of modules is not reverted.
+
+This test ensures that the lazy import of modules is not reverted.
+"""
+import copy
+import gc
+import sys
+
+import pytest
+
+from sky.adaptors.common import LazyImport
path: tests/unit_tests/test_sky_import.py
text: @user1:
style nit: import modules not class

@author:
OK, thanks!