You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by pi...@apache.org on 2022/01/25 04:35:26 UTC

[submarine] branch master updated: SUBMARINE-1157. Serving model via Python API or CLI

This is an automated email from the ASF dual-hosted git repository.

pingsutw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/submarine.git


The following commit(s) were added to refs/heads/master by this push:
     new b8284c0  SUBMARINE-1157. Serving model via Python API or CLI
b8284c0 is described below

commit b8284c00d71a4c875b0975de24d74f00bb0aa181
Author: KUAN-HSUN-LI <b0...@ntu.edu.tw>
AuthorDate: Wed Jan 12 21:13:01 2022 +0800

    SUBMARINE-1157. Serving model via Python API or CLI
    
    ### What is this PR for?
    Create the Python API and CLI command for serving a registered model.
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    * [x] - Save Pytorch model via torchscript
    * [x] - Generate the Serve API
    * [x] - Transfer the model description file to the config which is used in the Triton Inference Server. (It will be handled in this issue https://issues.apache.org/jira/browse/SUBMARINE-1173)
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-1157
    ### How should this be tested?
    The tests will provide in another PR because the transfer of the config is not finished.
    ### Screenshots (if appropriate)
    https://user-images.githubusercontent.com/38066413/147928471-7e8cc70b-99df-4b49-89bb-bc1627bbb885.mp4
    
    ### Questions:
    * Do the license files need updating? No
    * Are there breaking changes for older versions? No
    * Does this need new documentation? Yes
    
    Author: KUAN-HSUN-LI <b0...@ntu.edu.tw>
    
    Signed-off-by: Kevin <pi...@apache.org>
    
    Closes #860 from KUAN-HSUN-LI/SUBMARINE-1157 and squashes the following commits:
    
    06889e1e [KUAN-HSUN-LI] SUBMARINE-1157. fix
    7ab6b043 [KUAN-HSUN-LI] SUBMARINE-1157. fix
    04401830 [KUAN-HSUN-LI] SUBMARINE-1157. Python API for searving
    99bfd889 [KUAN-HSUN-LI] SUBMARINE-1157. Python API for searving
    fb61ade8 [KUAN-HSUN-LI] SUBMARINE-1157. Generate the serve API
---
 dev-support/pysubmarine/openapi.json               | 996 +++++++++++++++++++++
 submarine-sdk/pysubmarine/setup.py                 |   2 +-
 submarine-sdk/pysubmarine/submarine/cli/main.py    |  11 +
 .../pysubmarine/submarine/cli/serve/__init__.py    |  25 +
 .../pysubmarine/submarine/cli/serve/command.py     | 128 +++
 .../pysubmarine/submarine/client/__init__.py       |   2 +
 .../pysubmarine/submarine/client/api/__init__.py   |   1 +
 .../submarine/client/api/experiment_client.py      |  20 +-
 .../pysubmarine/submarine/client/api/serve_api.py  | 360 ++++++++
 .../submarine/client/api/serve_client.py           |  53 ++
 .../submarine/client/models/__init__.py            |   1 +
 .../submarine/client/models/serve_spec.py          | 223 +++++
 .../client/{api/__init__.py => utils/api_utils.py} |  27 +-
 .../pysubmarine/submarine/models/pytorch.py        |   6 +-
 .../pysubmarine/submarine/tracking/client.py       |  22 +-
 .../pysubmarine/submarine/tracking/fluent.py       |  19 +
 .../org/apache/submarine/server/Bootstrap.java     |   3 +-
 17 files changed, 1870 insertions(+), 29 deletions(-)

diff --git a/dev-support/pysubmarine/openapi.json b/dev-support/pysubmarine/openapi.json
new file mode 100644
index 0000000..0c25917
--- /dev/null
+++ b/dev-support/pysubmarine/openapi.json
@@ -0,0 +1,996 @@
+{
+  "openapi" : "3.0.1",
+  "info" : {
+    "title" : "Submarine API",
+    "description" : "The Submarine REST API allows you to access Submarine resources such as, \nexperiments, environments and notebooks. The \nAPI is hosted under the /v1 path on the Submarine server. For example, \nto list experiments on a server hosted at http://localhost:8080, access\nhttp://localhost:8080/api/v1/experiment/",
+    "termsOfService" : "http://swagger.io/terms/",
+    "contact" : {
+      "email" : "dev@submarine.apache.org"
+    },
+    "license" : {
+      "name" : "Apache 2.0",
+      "url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
+    },
+    "version" : "0.7.0-SNAPSHOT"
+  },
+  "servers" : [ {
+    "url" : "/api"
+  } ],
+  "paths" : {
+    "/v1/environment/{id}" : {
+      "get" : {
+        "tags" : [ "environment" ],
+        "summary" : "Find environment by name",
+        "operationId" : "getEnvironment",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/Environment"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Environment not found"
+          }
+        }
+      },
+      "delete" : {
+        "tags" : [ "environments" ],
+        "summary" : "Delete the environment",
+        "operationId" : "deleteEnvironment",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/Environment"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Environment not found"
+          }
+        }
+      },
+      "patch" : {
+        "tags" : [ "environments" ],
+        "summary" : "Update the environment with job spec",
+        "operationId" : "updateEnvironment",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/EnvironmentSpec"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/EnvironmentSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/Environment"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Environment not found"
+          }
+        }
+      }
+    },
+    "/v1/environment" : {
+      "get" : {
+        "tags" : [ "environments" ],
+        "summary" : "List of Environments",
+        "operationId" : "listEnvironment",
+        "parameters" : [ {
+          "name" : "status",
+          "in" : "query",
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/Environment"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post" : {
+        "tags" : [ "environment" ],
+        "summary" : "Create a environment",
+        "operationId" : "createEnvironment",
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/EnvironmentSpec"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/EnvironmentSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/Environment"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/experiment/logs/{id}" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Log experiment by id",
+        "operationId" : "getLog",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Experiment not found"
+          }
+        }
+      }
+    },
+    "/v1/experiment/ping" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Ping submarine server",
+        "description" : "Return the Pong message for test the connectivity",
+        "operationId" : "ping",
+        "responses" : {
+          "200" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "type" : "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/experiment" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "List experiments",
+        "operationId" : "listExperiments",
+        "parameters" : [ {
+          "name" : "status",
+          "in" : "query",
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Create an experiment",
+        "operationId" : "createExperiment",
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ExperimentSpec"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ExperimentSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/experiment/{name}" : {
+      "post" : {
+        "tags" : [ "experiment" ],
+        "summary" : "use experiment template to create an experiment",
+        "operationId" : "SubmitExperimentTemplate",
+        "parameters" : [ {
+          "name" : "name",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ExperimentTemplateSubmit"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ExperimentTemplateSubmit"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/experiment/{id}" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Get the experiment's detailed info by id",
+        "operationId" : "getExperiment",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Experiment not found"
+          }
+        }
+      },
+      "delete" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Delete the experiment",
+        "operationId" : "deleteExperiment",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Experiment not found"
+          }
+        }
+      },
+      "patch" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Update the experiment in the submarine server with spec",
+        "operationId" : "patchExperiment",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ExperimentSpec"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ExperimentSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Experiment not found"
+          }
+        }
+      }
+    },
+    "/v1/experiment/tensorboard" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Get tensorboard's information",
+        "operationId" : "getTensorboardInfo",
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Tensorboard not found"
+          }
+        }
+      }
+    },
+    "/v1/experiment/mlflow" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "Get mlflow's information",
+        "operationId" : "getMLflowInfo",
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "MLflow not found"
+          }
+        }
+      }
+    },
+    "/v1/experiment/logs" : {
+      "get" : {
+        "tags" : [ "experiment" ],
+        "summary" : "List experiment's log",
+        "operationId" : "listLog",
+        "parameters" : [ {
+          "name" : "status",
+          "in" : "query",
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/notebook/ping" : {
+      "get" : {
+        "tags" : [ "notebook" ],
+        "summary" : "Ping submarine server",
+        "description" : "Return the Pong message for test the connectivity",
+        "operationId" : "ping_1",
+        "responses" : {
+          "200" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "type" : "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/notebook" : {
+      "get" : {
+        "tags" : [ "notebook" ],
+        "summary" : "List notebooks",
+        "operationId" : "listNotebooks",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "query",
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "post" : {
+        "tags" : [ "notebook" ],
+        "summary" : "Create a notebook instance",
+        "operationId" : "createNotebook",
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/NotebookSpec"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/NotebookSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/notebook/{id}" : {
+      "get" : {
+        "tags" : [ "notebook" ],
+        "summary" : "Get detailed info about the notebook",
+        "operationId" : "getNotebook",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Notebook not found"
+          }
+        }
+      },
+      "delete" : {
+        "tags" : [ "notebook" ],
+        "summary" : "Delete the notebook",
+        "operationId" : "deleteNotebook",
+        "parameters" : [ {
+          "name" : "id",
+          "in" : "path",
+          "required" : true,
+          "schema" : {
+            "type" : "string"
+          }
+        } ],
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Notebook not found"
+          }
+        }
+      }
+    },
+    "/v1/serve/ping" : {
+      "get" : {
+        "tags" : [ "serve" ],
+        "summary" : "Ping submarine server",
+        "description" : "Return the Pong message for test the connectivity",
+        "operationId" : "ping_2",
+        "responses" : {
+          "200" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "type" : "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/v1/serve" : {
+      "post" : {
+        "tags" : [ "serve" ],
+        "summary" : "Create a serve instance",
+        "operationId" : "createServe",
+        "requestBody" : {
+          "content" : {
+            "application/yaml" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ServeSpec"
+              }
+            },
+            "application/json" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ServeSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          }
+        }
+      },
+      "delete" : {
+        "tags" : [ "serve" ],
+        "summary" : "Delete the serve instance.",
+        "operationId" : "deleteServe",
+        "requestBody" : {
+          "content" : {
+            "*/*" : {
+              "schema" : {
+                "$ref" : "#/components/schemas/ServeSpec"
+              }
+            }
+          }
+        },
+        "responses" : {
+          "default" : {
+            "description" : "successful operation",
+            "content" : {
+              "application/json; charset=utf-8" : {
+                "schema" : {
+                  "$ref" : "#/components/schemas/JsonResponse"
+                }
+              }
+            }
+          },
+          "404" : {
+            "description" : "Serve not found."
+          }
+        }
+      }
+    }
+  },
+  "components" : {
+    "schemas" : {
+      "Environment" : {
+        "type" : "object",
+        "properties" : {
+          "environmentId" : {
+            "$ref" : "#/components/schemas/EnvironmentId"
+          },
+          "environmentSpec" : {
+            "$ref" : "#/components/schemas/EnvironmentSpec"
+          }
+        }
+      },
+      "EnvironmentId" : {
+        "type" : "object",
+        "properties" : {
+          "id" : {
+            "type" : "integer",
+            "format" : "int32"
+          },
+          "serverTimestamp" : {
+            "type" : "integer",
+            "format" : "int64"
+          }
+        }
+      },
+      "EnvironmentSpec" : {
+        "type" : "object",
+        "properties" : {
+          "name" : {
+            "type" : "string"
+          },
+          "dockerImage" : {
+            "type" : "string"
+          },
+          "kernelSpec" : {
+            "$ref" : "#/components/schemas/KernelSpec"
+          },
+          "description" : {
+            "type" : "string"
+          },
+          "image" : {
+            "type" : "string"
+          }
+        }
+      },
+      "KernelSpec" : {
+        "type" : "object",
+        "properties" : {
+          "name" : {
+            "type" : "string"
+          },
+          "channels" : {
+            "type" : "array",
+            "items" : {
+              "type" : "string"
+            }
+          },
+          "condaDependencies" : {
+            "type" : "array",
+            "items" : {
+              "type" : "string"
+            }
+          },
+          "pipDependencies" : {
+            "type" : "array",
+            "items" : {
+              "type" : "string"
+            }
+          }
+        }
+      },
+      "JsonResponse" : {
+        "type" : "object",
+        "properties" : {
+          "code" : {
+            "type" : "integer",
+            "format" : "int32"
+          },
+          "success" : {
+            "type" : "boolean"
+          },
+          "result" : {
+            "type" : "object"
+          },
+          "attributes" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "type" : "object"
+            }
+          }
+        }
+      },
+      "CodeSpec" : {
+        "type" : "object",
+        "properties" : {
+          "syncMode" : {
+            "type" : "string"
+          },
+          "url" : {
+            "type" : "string"
+          }
+        }
+      },
+      "ExperimentMeta" : {
+        "type" : "object",
+        "properties" : {
+          "experimentId" : {
+            "type" : "string"
+          },
+          "name" : {
+            "type" : "string"
+          },
+          "namespace" : {
+            "type" : "string"
+          },
+          "framework" : {
+            "type" : "string"
+          },
+          "cmd" : {
+            "type" : "string"
+          },
+          "envVars" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "type" : "string"
+            }
+          },
+          "tags" : {
+            "type" : "array",
+            "items" : {
+              "type" : "string"
+            }
+          }
+        }
+      },
+      "ExperimentSpec" : {
+        "type" : "object",
+        "properties" : {
+          "meta" : {
+            "$ref" : "#/components/schemas/ExperimentMeta"
+          },
+          "environment" : {
+            "$ref" : "#/components/schemas/EnvironmentSpec"
+          },
+          "spec" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "$ref" : "#/components/schemas/ExperimentTaskSpec"
+            }
+          },
+          "code" : {
+            "$ref" : "#/components/schemas/CodeSpec"
+          }
+        }
+      },
+      "ExperimentTaskSpec" : {
+        "type" : "object",
+        "properties" : {
+          "replicas" : {
+            "type" : "integer",
+            "format" : "int32"
+          },
+          "resources" : {
+            "type" : "string"
+          },
+          "name" : {
+            "type" : "string"
+          },
+          "image" : {
+            "type" : "string"
+          },
+          "cmd" : {
+            "type" : "string"
+          },
+          "envVars" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "type" : "string"
+            }
+          },
+          "cpu" : {
+            "type" : "string"
+          },
+          "memory" : {
+            "type" : "string"
+          },
+          "gpu" : {
+            "type" : "string"
+          }
+        }
+      },
+      "ExperimentTemplateSubmit" : {
+        "type" : "object",
+        "properties" : {
+          "name" : {
+            "type" : "string"
+          },
+          "params" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "type" : "string"
+            }
+          }
+        }
+      },
+      "NotebookMeta" : {
+        "type" : "object",
+        "properties" : {
+          "name" : {
+            "type" : "string"
+          },
+          "namespace" : {
+            "type" : "string"
+          },
+          "ownerId" : {
+            "type" : "string"
+          },
+          "labels" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "type" : "string"
+            }
+          }
+        }
+      },
+      "NotebookPodSpec" : {
+        "type" : "object",
+        "properties" : {
+          "envVars" : {
+            "type" : "object",
+            "additionalProperties" : {
+              "type" : "string"
+            }
+          },
+          "resources" : {
+            "type" : "string"
+          },
+          "cpu" : {
+            "type" : "string"
+          },
+          "memory" : {
+            "type" : "string"
+          },
+          "gpu" : {
+            "type" : "string"
+          }
+        }
+      },
+      "NotebookSpec" : {
+        "type" : "object",
+        "properties" : {
+          "meta" : {
+            "$ref" : "#/components/schemas/NotebookMeta"
+          },
+          "environment" : {
+            "$ref" : "#/components/schemas/EnvironmentSpec"
+          },
+          "spec" : {
+            "$ref" : "#/components/schemas/NotebookPodSpec"
+          }
+        }
+      },
+      "ServeSpec" : {
+        "type" : "object",
+        "properties" : {
+          "modelName" : {
+            "type" : "string"
+          },
+          "modelVersion" : {
+            "type" : "integer",
+            "format" : "int32"
+          },
+          "modelType" : {
+            "type" : "string"
+          },
+          "modelURI" : {
+            "type" : "string"
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/submarine-sdk/pysubmarine/setup.py b/submarine-sdk/pysubmarine/setup.py
index 18abee4..bef6a7e 100644
--- a/submarine-sdk/pysubmarine/setup.py
+++ b/submarine-sdk/pysubmarine/setup.py
@@ -38,7 +38,7 @@ setup(
         "urllib3>=1.15.1",
         "certifi>=14.05.14",
         "python-dateutil>=2.5.3",
-        "pyarrow==0.17.0",
+        "pyarrow>=6.0.1",
         "boto3>=1.17.58",
         "click==8.0.3",
         "rich==10.15.2",
diff --git a/submarine-sdk/pysubmarine/submarine/cli/main.py b/submarine-sdk/pysubmarine/submarine/cli/main.py
index 2998f9a..3815f3a 100644
--- a/submarine-sdk/pysubmarine/submarine/cli/main.py
+++ b/submarine-sdk/pysubmarine/submarine/cli/main.py
@@ -22,6 +22,7 @@ from submarine.cli.environment import command as environment_cmd
 from submarine.cli.experiment import command as experiment_cmd
 from submarine.cli.notebook import command as notebook_cmd
 from submarine.cli.sandbox import command as sandbox_cmd
+from submarine.cli.serve import command as serve_cmd
 
 
 @click.group()
@@ -40,6 +41,11 @@ def cmdgrp_get():
     pass
 
 
+@entry_point.group("create")
+def cmdgrp_create():
+    pass
+
+
 @entry_point.group("delete")
 def cmdgrp_delete():
     pass
@@ -67,6 +73,11 @@ cmdgrp_delete.add_command(notebook_cmd.delete_notebook)
 cmdgrp_list.add_command(environment_cmd.list_environment)
 cmdgrp_get.add_command(environment_cmd.get_environment)
 cmdgrp_delete.add_command(environment_cmd.delete_environment)
+# # serve
+cmdgrp_list.add_command(serve_cmd.list_serve)
+cmdgrp_get.add_command(serve_cmd.get_serve)
+cmdgrp_create.add_command(serve_cmd.create_serve)
+cmdgrp_delete.add_command(serve_cmd.delete_serve)
 
 # sandbox
 cmdgrp_sandbox.add_command(sandbox_cmd.start_sandbox)
diff --git a/submarine-sdk/pysubmarine/submarine/cli/serve/__init__.py b/submarine-sdk/pysubmarine/submarine/cli/serve/__init__.py
new file mode 100644
index 0000000..95edd66
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/serve/__init__.py
@@ -0,0 +1,25 @@
+"""
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+"""
+
+from submarine.cli.serve.command import create_serve, delete_serve, get_serve, list_serve
+
+__all__ = [
+    "list_serve",
+    "get_serve",
+    "create_serve",
+    "delete_serve",
+]
diff --git a/submarine-sdk/pysubmarine/submarine/cli/serve/command.py b/submarine-sdk/pysubmarine/submarine/cli/serve/command.py
new file mode 100644
index 0000000..066e949
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/serve/command.py
@@ -0,0 +1,128 @@
+"""
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+"""
+import json
+import time
+
+import click
+from rich.console import Console
+from rich.json import JSON as richJSON
+from rich.panel import Panel
+
+from submarine.cli.config.config import loadConfig
+from submarine.client.api.serve_client import ServeClient
+from submarine.client.api_client import ApiException
+
+submarineCliConfig = loadConfig()
+serveClient = ServeClient(
+    f"http://{submarineCliConfig.connection.hostname}:{submarineCliConfig.connection.port}"
+    if submarineCliConfig
+    else "http://localhost:32080"
+)
+
+POLLING_INTERVAL = 1  # sec
+TIMEOUT = 30  # sec
+
+
+@click.command("serve")
+def list_serve():
+    """List serves"""
+    click.echo("The command is not supported yet.")  # TODO(kuanhsun)
+    click.echo("list serve!")
+
+
+@click.command("serve")
+@click.argument("model_name")
+@click.argument("model_version", type=int)
+def get_serve(model_name: str, model_version: int):
+    """Get serve"""
+    click.echo("The command is not supported yet.")  # TODO(kuanhsun)
+    click.echo(f"get serve! model name: {model_name}, model version: {model_version}")
+
+
+@click.command("serve")
+@click.argument("model_name")
+@click.argument("model_version", type=int)
+def create_serve(model_name: str, model_version: int):
+    """Create serve"""
+    console = Console()
+    try:
+        thread = serveClient.create_serve(model_name, model_version, async_req=True)
+        timeout = time.time() + TIMEOUT
+        with console.status(
+            f"[bold green] Creating Serve with name: {model_name}, version: {model_version}"
+        ):
+            while not thread.ready():
+                time.sleep(POLLING_INTERVAL)
+                if time.time() > timeout:
+                    console.print("[bold red] Timeout!")
+                    return
+        result = thread.get()
+        click.echo(result)
+
+    except ApiException as err:
+        if err.body is not None:
+            errbody = json.loads(err.body)
+            click.echo(f"[Api Error] {errbody['message']}")
+        else:
+            click.echo(f"[Api Error] {err}")
+
+
+@click.command("serve")
+@click.argument("model_name")
+@click.argument("model_version", type=int)
+@click.option("--wait", is_flag=True, default=False)
+def delete_serve(model_name: str, model_version: int, wait: bool):
+    """Delete serve"""
+    console = Console()
+    try:
+        thread = serveClient.delete_serve(model_name, model_version, async_req=True)
+        timeout = time.time() + TIMEOUT
+        with console.status(
+            f"[bold green] Deleting Serve with name: {model_name}, version: {model_version}"
+        ):
+            while not thread.ready():
+                time.sleep(POLLING_INTERVAL)
+                if time.time() > timeout:
+                    console.print("[bold red] Timeout!")
+                    return
+
+        result = thread.get()
+        click.echo(result)
+
+        if wait:
+            if result["status"] == "Deleted":
+                console.print(
+                    f"[bold green] Serve: model name:{model_name}, version: {model_version}"
+                )
+            else:
+                console.print("[bold red] Failed")
+                json_data = richJSON.from_data(result)
+                console.print(
+                    Panel(
+                        json_data,
+                        title=(
+                            f"[bold green] Serve Deleted: model name:{model_name}, version:"
+                            f" {model_version}"
+                        ),
+                    )
+                )
+    except ApiException as err:
+        if err.body is not None:
+            errbody = json.loads(err.body)
+            click.echo(f"[Api Error] {errbody['message']}")
+        else:
+            click.echo(f"[Api Error] {err}")
diff --git a/submarine-sdk/pysubmarine/submarine/client/__init__.py b/submarine-sdk/pysubmarine/submarine/client/__init__.py
index 47834d0..bec3f2d 100644
--- a/submarine-sdk/pysubmarine/submarine/client/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/__init__.py
@@ -36,6 +36,7 @@ __version__ = "0.7.0-SNAPSHOT"
 from submarine.client.api.environment_api import EnvironmentApi
 from submarine.client.api.experiment_api import ExperimentApi
 from submarine.client.api.notebook_api import NotebookApi
+from submarine.client.api.serve_api import ServeApi
 
 # import ApiClient
 from submarine.client.api_client import ApiClient
@@ -60,3 +61,4 @@ from submarine.client.models.kernel_spec import KernelSpec
 from submarine.client.models.notebook_meta import NotebookMeta
 from submarine.client.models.notebook_pod_spec import NotebookPodSpec
 from submarine.client.models.notebook_spec import NotebookSpec
+from submarine.client.models.serve_spec import ServeSpec
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py b/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
index 19defd6..741c497 100644
--- a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
@@ -19,5 +19,6 @@ from __future__ import absolute_import
 from submarine.client.api.environment_api import EnvironmentApi
 from submarine.client.api.experiment_api import ExperimentApi
 from submarine.client.api.notebook_api import NotebookApi
+from submarine.client.api.serve_api import ServeApi
 
 # flake8: noqa
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py b/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py
index 210b1df..1d6afb2 100644
--- a/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py
+++ b/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py
@@ -14,29 +14,16 @@
 # limitations under the License.
 
 import logging
-import os
 import time
 
 from submarine.client.api.experiment_api import ExperimentApi
-from submarine.client.api_client import ApiClient
-from submarine.client.configuration import Configuration
+from submarine.client.utils.api_utils import generate_host, get_api_client
 
 logger = logging.getLogger(__name__)
 logging.basicConfig(format="%(message)s")
 logging.getLogger().setLevel(logging.INFO)
 
 
-def generate_host():
-    """
-    Generate submarine host
-    :return: submarine host
-    """
-    submarine_server_dns_name = str(os.environ.get("SUBMARINE_SERVER_DNS_NAME"))
-    submarine_server_port = str(os.environ.get("SUBMARINE_SERVER_PORT"))
-    host = "http://" + submarine_server_dns_name + ":" + submarine_server_port
-    return host
-
-
 class ExperimentClient:
     def __init__(self, host: str = generate_host()):
         """
@@ -44,10 +31,7 @@ class ExperimentClient:
         :param host: An HTTP URI like http://submarine-server:8080.
         """
         # TODO(pingsutw): support authentication for talking to the submarine server
-        self.host = host
-        configuration = Configuration()
-        configuration.host = host + "/api"
-        api_client = ApiClient(configuration=configuration)
+        api_client = get_api_client(host)
         self.experiment_api = ExperimentApi(api_client=api_client)
 
     def create_experiment(self, experiment_spec):
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/serve_api.py b/submarine-sdk/pysubmarine/submarine/client/api/serve_api.py
new file mode 100644
index 0000000..803d060
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/client/api/serve_api.py
@@ -0,0 +1,360 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# coding: utf-8
+
+"""
+    Submarine API
+
+    The Submarine REST API allows you to access Submarine resources such as,  experiments, environments and notebooks. The  API is hosted under the /v1 path on the Submarine server. For example,  to list experiments on a server hosted at http://localhost:8080, access http://localhost:8080/api/v1/experiment/  # noqa: E501
+
+    The version of the OpenAPI document: 0.7.0-SNAPSHOT
+    Contact: dev@submarine.apache.org
+    Generated by: https://openapi-generator.tech
+"""
+
+
+from __future__ import absolute_import
+
+import re  # noqa: F401
+
+# python 2 and python 3 compatibility library
+import six
+
+from submarine.client.api_client import ApiClient
+from submarine.client.exceptions import ApiTypeError, ApiValueError  # noqa: F401
+
+
+class ServeApi(object):
+    """NOTE: This class is auto generated by OpenAPI Generator
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, api_client=None):
+        if api_client is None:
+            api_client = ApiClient()
+        self.api_client = api_client
+
+    def create_serve(self, **kwargs):  # noqa: E501
+        """Create a serve instance  # noqa: E501
+
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+        >>> thread = api.create_serve(async_req=True)
+        >>> result = thread.get()
+
+        :param async_req bool: execute request asynchronously
+        :param ServeSpec serve_spec:
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :return: JsonResponse
+                 If the method is called asynchronously,
+                 returns the request thread.
+        """
+        kwargs["_return_http_data_only"] = True
+        return self.create_serve_with_http_info(**kwargs)  # noqa: E501
+
+    def create_serve_with_http_info(self, **kwargs):  # noqa: E501
+        """Create a serve instance  # noqa: E501
+
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+        >>> thread = api.create_serve_with_http_info(async_req=True)
+        >>> result = thread.get()
+
+        :param async_req bool: execute request asynchronously
+        :param ServeSpec serve_spec:
+        :param _return_http_data_only: response data without head status code
+                                       and headers
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :return: tuple(JsonResponse, status_code(int), headers(HTTPHeaderDict))
+                 If the method is called asynchronously,
+                 returns the request thread.
+        """
+
+        local_var_params = locals()
+
+        all_params = ["serve_spec"]
+        all_params.extend(
+            ["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]
+        )
+
+        for key, val in six.iteritems(local_var_params["kwargs"]):
+            if key not in all_params:
+                raise ApiTypeError(
+                    "Got an unexpected keyword argument '%s' to method create_serve" % key
+                )
+            local_var_params[key] = val
+        del local_var_params["kwargs"]
+
+        collection_formats = {}
+
+        path_params = {}
+
+        query_params = []
+
+        header_params = {}
+
+        form_params = []
+        local_var_files = {}
+
+        body_params = None
+        if "serve_spec" in local_var_params:
+            body_params = local_var_params["serve_spec"]
+        # HTTP header `Accept`
+        header_params["Accept"] = self.api_client.select_header_accept(
+            ["application/json; charset=utf-8"]
+        )  # noqa: E501
+
+        # HTTP header `Content-Type`
+        header_params["Content-Type"] = self.api_client.select_header_content_type(  # noqa: E501
+            ["application/yaml", "application/json"]
+        )  # noqa: E501
+
+        # Authentication setting
+        auth_settings = []  # noqa: E501
+
+        return self.api_client.call_api(
+            "/v1/serve",
+            "POST",
+            path_params,
+            query_params,
+            header_params,
+            body=body_params,
+            post_params=form_params,
+            files=local_var_files,
+            response_type="JsonResponse",  # noqa: E501
+            auth_settings=auth_settings,
+            async_req=local_var_params.get("async_req"),
+            _return_http_data_only=local_var_params.get("_return_http_data_only"),  # noqa: E501
+            _preload_content=local_var_params.get("_preload_content", True),
+            _request_timeout=local_var_params.get("_request_timeout"),
+            collection_formats=collection_formats,
+        )
+
+    def delete_serve(self, **kwargs):  # noqa: E501
+        """Delete the serve instance.  # noqa: E501
+
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+        >>> thread = api.delete_serve(async_req=True)
+        >>> result = thread.get()
+
+        :param async_req bool: execute request asynchronously
+        :param ServeSpec serve_spec:
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :return: JsonResponse
+                 If the method is called asynchronously,
+                 returns the request thread.
+        """
+        kwargs["_return_http_data_only"] = True
+        return self.delete_serve_with_http_info(**kwargs)  # noqa: E501
+
+    def delete_serve_with_http_info(self, **kwargs):  # noqa: E501
+        """Delete the serve instance.  # noqa: E501
+
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+        >>> thread = api.delete_serve_with_http_info(async_req=True)
+        >>> result = thread.get()
+
+        :param async_req bool: execute request asynchronously
+        :param ServeSpec serve_spec:
+        :param _return_http_data_only: response data without head status code
+                                       and headers
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :return: tuple(JsonResponse, status_code(int), headers(HTTPHeaderDict))
+                 If the method is called asynchronously,
+                 returns the request thread.
+        """
+
+        local_var_params = locals()
+
+        all_params = ["serve_spec"]
+        all_params.extend(
+            ["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]
+        )
+
+        for key, val in six.iteritems(local_var_params["kwargs"]):
+            if key not in all_params:
+                raise ApiTypeError(
+                    "Got an unexpected keyword argument '%s' to method delete_serve" % key
+                )
+            local_var_params[key] = val
+        del local_var_params["kwargs"]
+
+        collection_formats = {}
+
+        path_params = {}
+
+        query_params = []
+
+        header_params = {}
+
+        form_params = []
+        local_var_files = {}
+
+        body_params = None
+        if "serve_spec" in local_var_params:
+            body_params = local_var_params["serve_spec"]
+        # HTTP header `Accept`
+        header_params["Accept"] = self.api_client.select_header_accept(
+            ["application/json; charset=utf-8"]
+        )  # noqa: E501
+
+        # Authentication setting
+        auth_settings = []  # noqa: E501
+
+        return self.api_client.call_api(
+            "/v1/serve",
+            "DELETE",
+            path_params,
+            query_params,
+            header_params,
+            body=body_params,
+            post_params=form_params,
+            files=local_var_files,
+            response_type="JsonResponse",  # noqa: E501
+            auth_settings=auth_settings,
+            async_req=local_var_params.get("async_req"),
+            _return_http_data_only=local_var_params.get("_return_http_data_only"),  # noqa: E501
+            _preload_content=local_var_params.get("_preload_content", True),
+            _request_timeout=local_var_params.get("_request_timeout"),
+            collection_formats=collection_formats,
+        )
+
+    def ping2(self, **kwargs):  # noqa: E501
+        """Ping submarine server  # noqa: E501
+
+        Return the Pong message for test the connectivity  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+        >>> thread = api.ping2(async_req=True)
+        >>> result = thread.get()
+
+        :param async_req bool: execute request asynchronously
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :return: str
+                 If the method is called asynchronously,
+                 returns the request thread.
+        """
+        kwargs["_return_http_data_only"] = True
+        return self.ping2_with_http_info(**kwargs)  # noqa: E501
+
+    def ping2_with_http_info(self, **kwargs):  # noqa: E501
+        """Ping submarine server  # noqa: E501
+
+        Return the Pong message for test the connectivity  # noqa: E501
+        This method makes a synchronous HTTP request by default. To make an
+        asynchronous HTTP request, please pass async_req=True
+        >>> thread = api.ping2_with_http_info(async_req=True)
+        >>> result = thread.get()
+
+        :param async_req bool: execute request asynchronously
+        :param _return_http_data_only: response data without head status code
+                                       and headers
+        :param _preload_content: if False, the urllib3.HTTPResponse object will
+                                 be returned without reading/decoding response
+                                 data. Default is True.
+        :param _request_timeout: timeout setting for this request. If one
+                                 number provided, it will be total request
+                                 timeout. It can also be a pair (tuple) of
+                                 (connection, read) timeouts.
+        :return: tuple(str, status_code(int), headers(HTTPHeaderDict))
+                 If the method is called asynchronously,
+                 returns the request thread.
+        """
+
+        local_var_params = locals()
+
+        all_params = []
+        all_params.extend(
+            ["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]
+        )
+
+        for key, val in six.iteritems(local_var_params["kwargs"]):
+            if key not in all_params:
+                raise ApiTypeError("Got an unexpected keyword argument '%s' to method ping2" % key)
+            local_var_params[key] = val
+        del local_var_params["kwargs"]
+
+        collection_formats = {}
+
+        path_params = {}
+
+        query_params = []
+
+        header_params = {}
+
+        form_params = []
+        local_var_files = {}
+
+        body_params = None
+        # HTTP header `Accept`
+        header_params["Accept"] = self.api_client.select_header_accept(
+            ["application/json; charset=utf-8"]
+        )  # noqa: E501
+
+        # Authentication setting
+        auth_settings = []  # noqa: E501
+
+        return self.api_client.call_api(
+            "/v1/serve/ping",
+            "GET",
+            path_params,
+            query_params,
+            header_params,
+            body=body_params,
+            post_params=form_params,
+            files=local_var_files,
+            response_type="str",  # noqa: E501
+            auth_settings=auth_settings,
+            async_req=local_var_params.get("async_req"),
+            _return_http_data_only=local_var_params.get("_return_http_data_only"),  # noqa: E501
+            _preload_content=local_var_params.get("_preload_content", True),
+            _request_timeout=local_var_params.get("_request_timeout"),
+            collection_formats=collection_formats,
+        )
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/serve_client.py b/submarine-sdk/pysubmarine/submarine/client/api/serve_client.py
new file mode 100644
index 0000000..545ea1f
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/client/api/serve_client.py
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from submarine.client.api.serve_api import ServeApi
+from submarine.client.utils.api_utils import generate_host, get_api_client
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(format="%(message)s")
+logging.getLogger().setLevel(logging.INFO)
+
+
+class ServeClient:
+    def __init__(self, host: str = generate_host()) -> None:
+        """
+        Submarine serve client constructor
+        :param host: An HTTP URI like http://submarine-server:8080.
+        """
+        api_client = get_api_client(host)
+        self.serve_api = ServeApi(api_client=api_client)
+
+    def create_serve(self, model_name: str, model_version: int, async_req: bool):
+        """
+        Create a model serve
+        :param model_name: Name of a registered model
+        :param model_version: Version of a registered model
+        """
+        serve_spec = {"modelName": model_name, "modelVersion": model_version}
+        response = self.serve_api.create_serve(serve_spec=serve_spec, async_req=async_req)
+        return response
+
+    def delete_serve(self, model_name: str, model_version: int, async_req: bool):
+        """
+        Delete a serving model
+        :param model_name: Name of a registered model
+        :param model_version: Version of a registered model
+        """
+        serve_spec = {"modelName": model_name, "modelVersion": model_version}
+        response = self.serve_api.delete_serve(serve_spec=serve_spec, async_req=async_req)
+        return response
diff --git a/submarine-sdk/pysubmarine/submarine/client/models/__init__.py b/submarine-sdk/pysubmarine/submarine/client/models/__init__.py
index 005b99b..0365a1d 100644
--- a/submarine-sdk/pysubmarine/submarine/client/models/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/models/__init__.py
@@ -41,3 +41,4 @@ from submarine.client.models.kernel_spec import KernelSpec
 from submarine.client.models.notebook_meta import NotebookMeta
 from submarine.client.models.notebook_pod_spec import NotebookPodSpec
 from submarine.client.models.notebook_spec import NotebookSpec
+from submarine.client.models.serve_spec import ServeSpec
diff --git a/submarine-sdk/pysubmarine/submarine/client/models/serve_spec.py b/submarine-sdk/pysubmarine/submarine/client/models/serve_spec.py
new file mode 100644
index 0000000..c91e4b6
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/client/models/serve_spec.py
@@ -0,0 +1,223 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# coding: utf-8
+
+"""
+    Submarine API
+
+    The Submarine REST API allows you to access Submarine resources such as,  experiments, environments and notebooks. The  API is hosted under the /v1 path on the Submarine server. For example,  to list experiments on a server hosted at http://localhost:8080, access http://localhost:8080/api/v1/experiment/  # noqa: E501
+
+    The version of the OpenAPI document: 0.7.0-SNAPSHOT
+    Contact: dev@submarine.apache.org
+    Generated by: https://openapi-generator.tech
+"""
+
+
+import pprint
+import re  # noqa: F401
+
+import six
+
+from submarine.client.configuration import Configuration
+
+
+class ServeSpec(object):
+    """NOTE: This class is auto generated by OpenAPI Generator.
+    Ref: https://openapi-generator.tech
+
+    Do not edit the class manually.
+    """
+
+    """
+    Attributes:
+      openapi_types (dict): The key is attribute name
+                            and the value is attribute type.
+      attribute_map (dict): The key is attribute name
+                            and the value is json key in definition.
+    """
+    openapi_types = {
+        "model_name": "str",
+        "model_version": "int",
+        "model_type": "str",
+        "model_uri": "str",
+    }
+
+    attribute_map = {
+        "model_name": "modelName",
+        "model_version": "modelVersion",
+        "model_type": "modelType",
+        "model_uri": "modelURI",
+    }
+
+    def __init__(
+        self,
+        model_name=None,
+        model_version=None,
+        model_type=None,
+        model_uri=None,
+        local_vars_configuration=None,
+    ):  # noqa: E501
+        """ServeSpec - a model defined in OpenAPI"""  # noqa: E501
+        if local_vars_configuration is None:
+            local_vars_configuration = Configuration()
+        self.local_vars_configuration = local_vars_configuration
+
+        self._model_name = None
+        self._model_version = None
+        self._model_type = None
+        self._model_uri = None
+        self.discriminator = None
+
+        if model_name is not None:
+            self.model_name = model_name
+        if model_version is not None:
+            self.model_version = model_version
+        if model_type is not None:
+            self.model_type = model_type
+        if model_uri is not None:
+            self.model_uri = model_uri
+
+    @property
+    def model_name(self):
+        """Gets the model_name of this ServeSpec.  # noqa: E501
+
+
+        :return: The model_name of this ServeSpec.  # noqa: E501
+        :rtype: str
+        """
+        return self._model_name
+
+    @model_name.setter
+    def model_name(self, model_name):
+        """Sets the model_name of this ServeSpec.
+
+
+        :param model_name: The model_name of this ServeSpec.  # noqa: E501
+        :type: str
+        """
+
+        self._model_name = model_name
+
+    @property
+    def model_version(self):
+        """Gets the model_version of this ServeSpec.  # noqa: E501
+
+
+        :return: The model_version of this ServeSpec.  # noqa: E501
+        :rtype: int
+        """
+        return self._model_version
+
+    @model_version.setter
+    def model_version(self, model_version):
+        """Sets the model_version of this ServeSpec.
+
+
+        :param model_version: The model_version of this ServeSpec.  # noqa: E501
+        :type: int
+        """
+
+        self._model_version = model_version
+
+    @property
+    def model_type(self):
+        """Gets the model_type of this ServeSpec.  # noqa: E501
+
+
+        :return: The model_type of this ServeSpec.  # noqa: E501
+        :rtype: str
+        """
+        return self._model_type
+
+    @model_type.setter
+    def model_type(self, model_type):
+        """Sets the model_type of this ServeSpec.
+
+
+        :param model_type: The model_type of this ServeSpec.  # noqa: E501
+        :type: str
+        """
+
+        self._model_type = model_type
+
+    @property
+    def model_uri(self):
+        """Gets the model_uri of this ServeSpec.  # noqa: E501
+
+
+        :return: The model_uri of this ServeSpec.  # noqa: E501
+        :rtype: str
+        """
+        return self._model_uri
+
+    @model_uri.setter
+    def model_uri(self, model_uri):
+        """Sets the model_uri of this ServeSpec.
+
+
+        :param model_uri: The model_uri of this ServeSpec.  # noqa: E501
+        :type: str
+        """
+
+        self._model_uri = model_uri
+
+    def to_dict(self):
+        """Returns the model properties as a dict"""
+        result = {}
+
+        for attr, _ in six.iteritems(self.openapi_types):
+            value = getattr(self, attr)
+            if isinstance(value, list):
+                result[attr] = list(
+                    map(lambda x: x.to_dict() if hasattr(x, "to_dict") else x, value)
+                )
+            elif hasattr(value, "to_dict"):
+                result[attr] = value.to_dict()
+            elif isinstance(value, dict):
+                result[attr] = dict(
+                    map(
+                        lambda item: (item[0], item[1].to_dict())
+                        if hasattr(item[1], "to_dict")
+                        else item,
+                        value.items(),
+                    )
+                )
+            else:
+                result[attr] = value
+
+        return result
+
+    def to_str(self):
+        """Returns the string representation of the model"""
+        return pprint.pformat(self.to_dict())
+
+    def __repr__(self):
+        """For `print` and `pprint`"""
+        return self.to_str()
+
+    def __eq__(self, other):
+        """Returns true if both objects are equal"""
+        if not isinstance(other, ServeSpec):
+            return False
+
+        return self.to_dict() == other.to_dict()
+
+    def __ne__(self, other):
+        """Returns true if both objects are not equal"""
+        if not isinstance(other, ServeSpec):
+            return True
+
+        return self.to_dict() != other.to_dict()
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py b/submarine-sdk/pysubmarine/submarine/client/utils/api_utils.py
similarity index 54%
copy from submarine-sdk/pysubmarine/submarine/client/api/__init__.py
copy to submarine-sdk/pysubmarine/submarine/client/utils/api_utils.py
index 19defd6..9a7f6ae 100644
--- a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/utils/api_utils.py
@@ -13,11 +13,26 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from __future__ import absolute_import
+import os
 
-# import apis into api package
-from submarine.client.api.environment_api import EnvironmentApi
-from submarine.client.api.experiment_api import ExperimentApi
-from submarine.client.api.notebook_api import NotebookApi
+from submarine.client.api_client import ApiClient
+from submarine.client.configuration import Configuration
 
-# flake8: noqa
+
+def generate_host() -> str:
+    """
+    Generate submarine host
+    :return: submarine host
+    """
+    submarine_server_dns_name = str(os.environ.get("SUBMARINE_SERVER_DNS_NAME"))
+    submarine_server_port = str(os.environ.get("SUBMARINE_SERVER_PORT"))
+    host = "http://" + submarine_server_dns_name + ":" + submarine_server_port
+    return host
+
+
+def get_api_client(host: str) -> ApiClient:
+    configuration = Configuration()
+    configuration.host = host + "/api"
+    api_client = ApiClient(configuration=configuration)
+
+    return api_client
diff --git a/submarine-sdk/pysubmarine/submarine/models/pytorch.py b/submarine-sdk/pysubmarine/submarine/models/pytorch.py
index 38cdd57..1b45d30 100644
--- a/submarine-sdk/pysubmarine/submarine/models/pytorch.py
+++ b/submarine-sdk/pysubmarine/submarine/models/pytorch.py
@@ -18,5 +18,7 @@ import os
 import torch
 
 
-def save_model(model, artifact_path: str):
-    torch.save(model, os.path.join(artifact_path, "model.pth"))
+def save_model(model, artifact_path: str, input_dim: list) -> None:
+    example_forward_example = torch.rand(input_dim)
+    scripted_model = torch.jit.trace(model, example_forward_example)
+    scripted_model.save(model, os.path.join(artifact_path, "model.pth"))
diff --git a/submarine-sdk/pysubmarine/submarine/tracking/client.py b/submarine-sdk/pysubmarine/submarine/tracking/client.py
index f563254..76f350a 100644
--- a/submarine-sdk/pysubmarine/submarine/tracking/client.py
+++ b/submarine-sdk/pysubmarine/submarine/tracking/client.py
@@ -21,6 +21,8 @@ from typing import Any, Dict
 
 import submarine
 from submarine.artifacts.repository import Repository
+from submarine.client.api.serve_client import ServeClient
+from submarine.client.utils.api_utils import generate_host
 from submarine.entities import Metric, Param
 from submarine.exceptions import SubmarineException
 from submarine.tracking import utils
@@ -40,6 +42,7 @@ class SubmarineClient(object):
         s3_registry_uri: str = None,
         aws_access_key_id: str = None,
         aws_secret_access_key: str = None,
+        host: str = generate_host(),
     ) -> None:
         """
         :param db_uri: Address of local or remote tracking server. If not provided, defaults
@@ -54,6 +57,7 @@ class SubmarineClient(object):
         self.db_uri = db_uri or submarine.get_db_uri()
         self.store = utils.get_tracking_sqlalchemy_store(self.db_uri)
         self.model_registry = utils.get_model_registry_sqlalchemy_store(self.db_uri)
+        self.serve_client = ServeClient(host)
 
     def log_metric(
         self,
@@ -130,7 +134,7 @@ class SubmarineClient(object):
                         "Saving pytorch model needs to provide input and output dimension for"
                         " serving."
                     )
-                submarine.models.pytorch.save_model(model, model_save_dir)
+                submarine.models.pytorch.save_model(model, model_save_dir, input_dim)
             elif model_type == "tensorflow":
                 import submarine.models.tensorflow
 
@@ -171,3 +175,19 @@ class SubmarineClient(object):
                 experiment_id=utils.get_job_id(),
                 model_type=model_type,
             )
+
+    def create_serve(self, model_name: str, model_version: int, async_req: bool = True):
+        """
+        Create serve of a model through Seldon Core
+        :param model_name: Name of a registered model
+        :param model_version: Version of a registered model
+        """
+        return self.serve_client.create_serve(model_name, model_version, async_req)
+
+    def delete_serve(self, model_name: str, model_version: int, async_req: bool = True):
+        """
+        Delete a serving model
+        :param model_name: Name of a registered model
+        :param model_version: Version of a registered model
+        """
+        return self.serve_client.delete_serve(model_name, model_version, async_req=async_req)
diff --git a/submarine-sdk/pysubmarine/submarine/tracking/fluent.py b/submarine-sdk/pysubmarine/submarine/tracking/fluent.py
index dce8cfe..ec9ed9e 100644
--- a/submarine-sdk/pysubmarine/submarine/tracking/fluent.py
+++ b/submarine-sdk/pysubmarine/submarine/tracking/fluent.py
@@ -73,3 +73,22 @@ def save_model(
     SubmarineClient().save_model(
         model_type, model, artifact_path, registered_model_name, input_dim, output_dim
     )
+    SubmarineClient().save_model(model_type, model, artifact_path, registered_model_name)
+
+
+def create_serve(model_name: str, model_version: int):
+    """
+    Create serve of a model through Seldon Core
+    :param model_name: Name of a registered model
+    :param model_version: Version of a registered model
+    """
+    SubmarineClient().create_serve(model_name, model_version)
+
+
+def delete_serve(model_name: str, model_version: int):
+    """
+    Delete a serving model
+    :param model_name: Name of a registered model
+    :param model_version: Version of a registered model
+    """
+    SubmarineClient().delete_serve(model_name, model_version)
diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java
index 76e3ab4..a373558 100644
--- a/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java
+++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java
@@ -66,7 +66,8 @@ public class Bootstrap extends HttpServlet {
                     .collect(Collectors.toSet()))
             .resourceClasses(Stream.of("org.apache.submarine.server.rest.NotebookRestApi",
                     "org.apache.submarine.server.rest.ExperimentRestApi",
-                    "org.apache.submarine.server.rest.EnvironmentRestApi")
+                    "org.apache.submarine.server.rest.EnvironmentRestApi",
+                    "org.apache.submarine.server.rest.ServeRestApi")
                     .collect(Collectors.toSet()));
 
     try {

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org