Lightning-AI / pytorch-lightning

Pretrain, finetune and deploy AI models on multiple GPUs, TPUs with zero code changes.

Home Page:https://lightning.ai

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

LightningFlow expects too fast an initial response from other servers: Cannot display holoviews plots with Panel

MarcSkovMadsen opened this issue Β· comments

Crosspost of holoviz/panel#3643 as I don't know if this is a Panel/HoloViews issue or a lightning.ai issue

πŸ› Bug

I'm trying to create a Panel+HoloViz crossfiltering example with lightning.ai. I can create other examples. See #13335, but the HoloViews one never serves.

To Reproduce

pip install panel==0.13.1 bokeh==2.4.3 lightning==2022.6.15.post2 holoviews==1.14.9

app.py

# Source: https://github.com/Lightning-AI/lightning/blob/master/docs/source-app/workflows/build_lightning_app/from_scratch.rst
# Source: https://github.com/Lightning-AI/lightning/blob/master/src/lightning_app/frontend/stream_lit.py
from __future__ import annotations

import pathlib
from typing import Callable, Dict, Union

import holoviews as hv
import lightning_app as lapp
import panel as pn
from bokeh.sampledata import iris
from bokeh.server.server import Server
from lightning_app.core import constants

Page = Union[Callable[[], pn.viewable.Viewable], str, pathlib.Path]

def page_crossfilter():
    pn.extension(sizing_mode="stretch_width")
    hv.extension("bokeh")
    scatter = hv.Scatter(iris.flowers, kdims=["sepal_length"], vdims=["sepal_width"]).opts(responsive=True)
    
    return pn.pane.HoloViews(scatter, sizing_mode="stretch_both")

class LitPanelPage(lapp.LightningWork):
    """Can serve a single page Panel app"""

    def __init__(self, page: Page, **params):
        """_summary_

        Args:
            page (Callable[[], pn.viewable.Viewable]): A function returning a Panel `Viewable`.
                Alternatively a path to a file containing a Panel application.
        """
        if isinstance(page, pathlib.Path):
            page = str(page)
        self._page = page

        self._server: Union[None, Server] = None

        super().__init__(**params)

    def run(self):
        """Starts the server and serves the page"""
        self._server = pn.serve(
            {"/": self._page},
            port=self.port,
            address=self.host,
            websocket_origin=self.websocket_origin,
            show=False,
        )

    def stop(self):
        """Stops the server"""
        if self._server:
            self._server.stop()

    def get_tab(self, name: str) -> Dict[str, str | "LitPanelPage"]:
        """Returns a *tab* definition to be included in the layout of a LightingFlow"""
        return {"name": name, "content": self}

    @property
    def websocket_origin(self) -> str:
        host = constants.APP_SERVER_HOST.replace("http://", "").replace("https://", "")
        return host + ":" + str(self.port)


class LitApp(lapp.LightningFlow):
    def __init__(self):
        super().__init__()
        self.lit_crossfilter = LitPanelPage(page=page_crossfilter, parallel=True)

    def run(self):
        self.lit_crossfilter.run()

    def configure_layout(self):
        return [
            self.lit_crossfilter.get_tab(name="Crossfiltering"),
        ]


if __name__ == "__main__":
    # lightning run app app.py
    app = lapp.LightningApp(LitApp())
elif __name__.startswith("bokeh"):
    # panel serve app.py --autoreload --show
    page_crossfilter().servable()

run with

$ lightning run app app.py
Your Lightning App is starting. This won't take long.
INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view
Launching server at http://127.0.0.1:62458

And see it loads forever

image

If instead I do

$ panel serve app.py
2022-06-23 07:33:25,018 Starting Bokeh server version 2.4.3 (running on Tornado 6.1)
2022-06-23 07:33:25,019 User authentication hooks NOT provided (default user enabled)
2022-06-23 07:33:25,026 Bokeh app running at: http://localhost:5006/app
2022-06-23 07:33:25,026 Starting Bokeh server with process id: 31292
2022-06-23 07:33:29,603 WebSocket connection opened
2022-06-23 07:33:29,603 ServerConnection created

It works

image

This works for me flawlessly on the latest version of Lightning (the one you posted should be). Screenshot of the running app:

image

It takes 1-2 secs for it to become visible on the UI after the spinner. Perhaps you need to manually reload the page?
Could you check your console (in the browser) for any errors (cmd+option+i on mac).

Thanks @awaelchli

I'm on Windows. Could that make a difference?

The 1 min video below shows how it looks and that it never starts. My fan goes crazy while the server is running. So something is happening.

holoviews-not-working.mp4

I don't see any errors in the browsers console either

image

I would like to try out running it in Docker to see it working on Linux at least.

Dockerfile

FROM python:3.9.13-slim-bullseye

RUN pip install panel==0.13.1 bokeh==2.4.3 lightning==2022.6.15.post2 holoviews==1.14.9

ADD . .

ENTRYPOINT [ "lightning", "run", "app", "app.py" ]
docker build -f Dockerfile -t lightning .
docker run --expose 1-65000 lightning

How do I run the app on the address 0.0.0.0 instead of 127.0.0.1 to be able to access the app outside the docker container?

I can see that the function page_crossfilter starts running infinetely if I add a print statement

# Source: https://github.com/Lightning-AI/lightning/blob/master/docs/source-app/workflows/build_lightning_app/from_scratch.rst
# Source: https://github.com/Lightning-AI/lightning/blob/master/src/lightning_app/frontend/stream_lit.py
from __future__ import annotations

import pathlib
from typing import Callable, Dict, Union

import holoviews as hv
import lightning_app as lapp
import pandas as pd
import panel as pn
from bokeh.sampledata import iris
from bokeh.server.server import Server
from lightning_app.core import constants

Page = Union[Callable[[], pn.viewable.Viewable], str, pathlib.Path]

def page_crossfilter():
    print("creating plot")
    pn.extension(sizing_mode="stretch_width")
    hv.extension("bokeh")
    scatter = hv.Scatter(iris.flowers, kdims=["sepal_length"], vdims=["sepal_width"]).opts(responsive=True)
    return pn.pane.HoloViews(scatter, sizing_mode="stretch_both")

class LitPanelPage(lapp.LightningWork):
    """Can serve a single page Panel app"""

    def __init__(self, page: Page, **params):
        """_summary_

        Args:
            page (Callable[[], pn.viewable.Viewable]): A function returning a Panel `Viewable`.
                Alternatively a path to a file containing a Panel application.
        """
        if isinstance(page, pathlib.Path):
            page = str(page)
        self._page = page

        self._server: Union[None, Server] = None

        super().__init__(**params)

    def run(self):
        """Starts the server and serves the page"""
        self._server = pn.serve(
            {"/": self._page},
            port=self.port,
            address=self.host,
            websocket_origin=self.websocket_origin,
            show=False,
        )

    def stop(self):
        """Stops the server"""
        if self._server:
            self._server.stop()

    def get_tab(self, name: str) -> Dict[str, str | "LitPanelPage"]:
        """Returns a *tab* definition to be included in the layout of a LightingFlow"""
        return {"name": name, "content": self}

    @property
    def websocket_origin(self) -> str:
        host = constants.APP_SERVER_HOST.replace("http://", "").replace("https://", "")
        return host + ":" + str(self.port)


class LitApp(lapp.LightningFlow):
    def __init__(self):
        super().__init__()
        self.lit_crossfilter = LitPanelPage(page=page_crossfilter, parallel=True)

    def run(self):
        self.lit_crossfilter.run()

    def configure_layout(self):
        return [
            self.lit_crossfilter.get_tab(name="Crossfiltering"),
        ]


if __name__ == "__main__":
    # lightning run app app.py
    app = lapp.LightningApp(LitApp())
elif __name__.startswith("bokeh"):
    # panel serve app.py --autoreload --show
    page_crossfilter().servable()
$  lightning run app app.py
Your Lightning App is starting. This won't take long.
INFO: Your app has started. View it in your browser: http://127.0.0.1:7501/view
Launching server at http://127.0.0.1:52479
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
creating plot
...
creating-plot.mp4

Could it be @awaelchli that the lightning server requests a response from the Panel server, and that response is not coming as fast as anticipated, then it requests a response again and continues like that forever and the Panel server gets slowed down? The Panel server is not your usual FastApi server. The initial response can be quite big. The purpose is not to be fast but to be really powerful for specialized data science use cases.

If I run the panel server in a separate process like below via Popen I would expect the server to run more independent of the lightning ai code. There should be no problems with threads, multiprocessing on windows etc. But I still see an infinite number of requests to the Panel page. Where are they coming from?

app.py

from __future__ import annotations

import subprocess
from typing import Dict

import lightning_app as lapp

Page = str

class LitPanelPage(lapp.LightningWork):
    """Can serve a single page Panel app"""

    def __init__(self, page: Page, **params):
        """_summary_

        Args:
            page (Callable[[], pn.viewable.Viewable]): A function returning a Panel `Viewable`.
                Alternatively a path to a file containing a Panel application.
        """
        self._page = page

        self._server: subprocess.Popen = None

        super().__init__(**params)

    def run(self):
        """Starts the server and serves the page"""
        subprocess.Popen(
            [
                "panel",
                "serve",
                "app2.py",
                "--port",
                str(self.port),
                "--address",
                self.host,
                "--allow-websocket-origin",
                "*"
            ],
        )

    def stop(self):
        """Stops the server"""
        self._server.kill()

    def get_tab(self, name: str) -> Dict[str, str | "LitPanelPage"]:
        """Returns a *tab* definition to be included in the layout of a LightingFlow"""
        return {"name": name, "content": self}

class LitApp(lapp.LightningFlow):
    def __init__(self):
        super().__init__()
        self.lit_crossfilter = LitPanelPage(page="app2.py", parallel=True)

    def run(self):
        self.lit_crossfilter.run()

    def configure_layout(self):
        return [
            self.lit_crossfilter.get_tab(name="Crossfiltering"),
        ]


if __name__ == "__main__":
    # lightning run app app.py
    app = lapp.LightningApp(LitApp())

app2.py

# Source: https://github.com/Lightning-AI/lightning/blob/master/docs/source-app/workflows/build_lightning_app/from_scratch.rst
# Source: https://github.com/Lightning-AI/lightning/blob/master/src/lightning_app/frontend/stream_lit.py
print("starting")

import holoviews as hv
import panel as pn
from holoviews import opts
from bokeh.sampledata import iris

ACCENT = "#792EE5"
OPTS = {
    "all": {
        "scatter": {"color": ACCENT, "responsive": True, "size": 10},
        "hist": {"color": ACCENT, "responsive": True},
    },
    "bokeh": {
        "scatter": {"tools": ["hover"], "active_tools": ["box_select"]},
        "hist": {"tools": ["hover"], "active_tools": ["box_select"]},
    },
}


pn.extension(sizing_mode="stretch_width")
hv.extension("bokeh")


dataset = hv.Dataset(iris.flowers)

scatter = hv.Scatter(dataset, kdims=["sepal_length"], vdims=["sepal_width"]).opts(responsive=True)
pn.pane.HoloViews(scatter, sizing_mode="stretch_both").servable()
lightning run app app.py
response-not-fast-enough.mp4

Hey @MarcSkovMadsen.

Wow ! Awesome work with https://github.com/MarcSkovMadsen/awesome-panel-lightning.
In order to fix this bug, we need to increase the timeout here to 0.5: https://github.com/Lightning-AI/lightning/blob/15b64cf8bac457e47a19102dd8152ddfb1821b72/src/lightning_app/utilities/network.py#L51.

We are in the process of migrating the internal repo to the public, so it might take us an extra week, but I opened a fixed PR to the private repo.

Once everything is done, I will make a PR to your repo with some minor improvements.

Otherwise, do you have feedback on the Lightning App framework? I think it would be interesting to use the dashboards to dynamically create some work with heavy lifting and use the results back in the UI.

Hi @tchaton

Thanks for your reply. Would it be possible for the application to set the timeout?

Some applications might need a timeout of several seconds. My experience from Panel and the Panel community is that for internal tools the time it takes to create and get the response is often not important. And also the applications in general might not be optimized for performance but instead to solve to bespoke domain problem for which there are no existing tools. And they are created by non-developers that don't have the skills to create performant applications.

I think lightning.ai looks promising for creating scalable apps and easily deploying them.

I would like to know how to run lighting in a Docker container to be able to test them on Linux (my laptop is windows). Basically I want to know how I can set the address to 0.0.0.0 instead of 127.0.0.1.

I would also like to start understanding how I can use Panel to create an application that interacts with flows running in different processes. It should be easy to do with Panel as it is a reactive framework running on Tornado, using web sockets and supporting async, threading etc.