Deploy DeepSeek-V4-Flash with SGLang and Modal
We’ll show in this example how to serve DeepSeek-V4-Flash, a Mixture-of-Experts (MoE) model with 284B total parameters and 13B active.
It achieves comparable reasoning performance to its bigger variant, the DeepSeek-V4-Pro preview, while being much more compact in terms of model parameters.
Set up the container image
An issue currently exists with the drafter incorrectly rewriting states. While not yet merged, we apply the fix in this open PR manually to the container image provided by the SGLang team.
import json
import shlex
import subprocess
import time
import urllib.error
import urllib.request
import modal
MINUTES = 60 # seconds
GB = 1024 # mb
PR32183_DIFF_URL = (
"https://github.com/sgl-project/sglang/compare/"
"5387e23ecd7dde4c383ae857983686e6a73bddf3..."
"22ef431215b1d8529eaebd8e8c6de9510390afaf.diff"
)
PR32183_DIFF_SHA256 = "ddd65902ba570c158f9d6783604cf7d9f2f13bf41994fcbf330a68ea1909923c"
sglang_image = (
modal.Image.from_registry("lmsysorg/sglang:nightly-dev-cu13-20260729-16a52bff")
.entrypoint([]) # silence chatty logs on container start
.run_commands(
f"curl -fsSL {PR32183_DIFF_URL} -o /tmp/pr32183.diff",
f"echo '{PR32183_DIFF_SHA256} /tmp/pr32183.diff' | sha256sum -c -",
"cd /sgl-workspace/sglang"
" && git apply --stat --exclude=test/* /tmp/pr32183.diff"
" && git apply --exclude=test/* /tmp/pr32183.diff",
)
)Load and cache the model weights and kernels
Downloads from the Hugging Face Hub are much faster if you are authenticated, so we add a Hugging Face token as a Modal Secret with:
modal secret create huggingface-secret HF_TOKEN=hf_...MODEL_NAME = "deepseek-ai/DeepSeek-V4-Flash-0731"
MODEL_REVISION = "9e165c30e2704aec5d9d593cce3eebd58bbef1cb"
hf_secret = modal.Secret.from_name("huggingface-secret")We don’t want to load the model from the Hub every time we start the server.
So instead, we load the cached weights from a Modal Volume.
Note that the container image already contains files at the default location /root/.cache/huggingface, so we specify a different path.
HF_CACHE_DIR = "/cache/huggingface"
hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True)We also want to turn on high performance downloads to fully saturate our network bandwidth.
sglang_image = sglang_image.env(
{"HF_HUB_CACHE": f"{HF_CACHE_DIR}/hub", "HF_XET_HIGH_PERFORMANCE": "1"}
)
def download_model(repo_id, revision=None):
from huggingface_hub import snapshot_download
snapshot_download(repo_id=repo_id, revision=revision, max_workers=16)
sglang_image = sglang_image.run_function(
download_model,
volumes={HF_CACHE_DIR: hf_cache_vol},
secrets=[hf_secret],
args=(MODEL_NAME, MODEL_REVISION),
timeout=4 * 60 * MINUTES,
cpu=8,
)As part of the loading process, the model compiles DeepGEMM and FlashInfer kernels. To avoid recompilation on cold-starts, we specify a path to a Volume for the compiled kernels to live in.
DG_CACHE_DIR = "/cache/deep_gemm"
FLASHINFER_CACHE_DIR = "/root/.cache/sglang/flashinfer"
dg_cache_vol = modal.Volume.from_name("sglang-deepgemm-cache", create_if_missing=True)
flashinfer_cache_vol = modal.Volume.from_name(
"flashinfer-autotune-cache", create_if_missing=True
)
sglang_image = sglang_image.env(
{
"SGLANG_DG_CACHE_DIR": DG_CACHE_DIR,
"SGLANG_JIT_DEEPGEMM_FAST_WARMUP": "1",
"TILELANG_CACHE_DIR": f"{DG_CACHE_DIR}/tilelang",
}
)Configure the infrastructure
We choose a GPU to deploy our inference server onto. Conveniently, a single B300 can hold the model weights, KV cache, and speculative decoding module. It offers excellent price-performance and supports both 8 bit and 4 bit quantized floating point operations.
GPU_TYPE, GPU_COUNT = "B300", 1
CPU = 8
MEMORY = 96 * GBFor production-scale LLM inference services, there are generally
enough requests to justify keeping at least one replica running at all times.
However, to ensure at least one container is always available,
we can set min_containers to 1 or more for our inference server.
MIN_CONTAINERS = 0 # set to 1 in production to keep a warm replicaModal empowers you to decide how to scale up and down replicas in response to load. Without autoscaling, users’ requests will queue when the server becomes overloaded or simply face higher latencies once above a certain minimum number of concurrent requests.
TARGET_INPUTS = 24Modal considers a new replica ready to receive inputs once the modal.enter methods have exited and the container accepts connections.
To ensure that our server is actually ready for inputs,
we define helper functions to check and ensure the server is ready
from both within the container and a local client.
STARTUP_TIMEOUT = 60 * MINUTES
def is_server_up(url: str) -> bool:
try:
with urllib.request.urlopen(url, timeout=5) as response:
return response.status == 200
except (urllib.error.URLError, OSError, TimeoutError):
return False
def wait_ready(proc: subprocess.Popen):
url = f"http://localhost:{DEFAULT_PORT}/health"
print(f"waiting for server to be ready at {url}")
while True:
if proc.poll() is not None:
raise RuntimeError(
f"SGLang exited with code {proc.returncode} before becoming healthy"
)
if is_server_up(url):
print("server is ready!")
return
time.sleep(5)
def warmup():
payload = {
"model": MODEL_NAME,
"messages": [{"role": "user", "content": "Hello, how are you?"}],
"max_tokens": 16,
}
for _ in range(3):
req = urllib.request.Request(
f"http://localhost:{DEFAULT_PORT}/v1/chat/completions",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5 * MINUTES) as resp:
resp.read()
except (urllib.error.URLError, OSError, TimeoutError) as exc:
print(f"warmup request failed, continuing: {exc}")
def wait_for_endpoint(url: str, timeout: int = STARTUP_TIMEOUT) -> None:
deadline = time.monotonic() + timeout
health = f"{url.rstrip('/')}/health"
while True:
if is_server_up(health):
return
if time.monotonic() >= deadline:
raise TimeoutError("Timed out waiting for the Server endpoint.")
time.sleep(5)Define the inference server
For maximum performance, we set a few bespoke enviroment variables and engine flags.
sglang_image = sglang_image.env(
{
"NCCL_CUMEM_ENABLE": "1",
"PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True",
"SGLANG_DEFAULT_THINKING": "false",
"SGLANG_TIMEOUT_KEEP_ALIVE": f"{5 * MINUTES}",
"TORCHINDUCTOR_COMPILE_THREADS": "1",
}
)
DEFAULT_PORT = 8000
def _server_command() -> list[str]:
cmd = [
"sglang",
"serve",
"--model-path",
MODEL_NAME,
"--served-model-name",
MODEL_NAME,
"--revision",
MODEL_REVISION,
"--host",
"0.0.0.0",
"--port",
str(DEFAULT_PORT),
"--tp",
str(GPU_COUNT),
"--chunked-prefill-size",
"4096",
"--context-length",
"268000",
"--cuda-graph-max-bs-decode",
"64",
"--decode-log-interval",
"200",
"--default-chat-template-kwargs",
'{"thinking":false}',
"--disable-flashinfer-autotune",
"--dist-timeout",
f"{60 * MINUTES}",
"--max-running-requests",
"64",
"--mem-fraction-static",
"0.90",
"--moe-a2a-backend",
"none",
"--moe-runner-backend",
"flashinfer_mxfp4",
"--reasoning-parser",
"deepseek-v4",
"--speculative-algorithm",
"DSPARK",
"--swa-full-tokens-ratio",
"0.1",
"--tool-call-parser",
"deepseekv4",
"--trust-remote-code",
"--skip-server-warmup",
]
return cmdOnto the main event that is defining our inference server.
app = modal.App(name="example-deepseek-v4-flash")
@app.server(
image=sglang_image,
gpu=f"{GPU_TYPE}:{GPU_COUNT}",
volumes={
HF_CACHE_DIR: hf_cache_vol,
DG_CACHE_DIR: dg_cache_vol,
FLASHINFER_CACHE_DIR: flashinfer_cache_vol,
},
cpu=CPU,
memory=MEMORY,
port=DEFAULT_PORT,
startup_timeout=STARTUP_TIMEOUT,
exit_grace_period=25, # seconds, time to finish up requests when closing down
min_containers=MIN_CONTAINERS,
target_concurrency=TARGET_INPUTS,
unauthenticated=True,
)
class Server:
@modal.enter()
def startup(self):
cmd = _server_command()
print(shlex.join(cmd))
self.proc = subprocess.Popen(cmd, start_new_session=True)
wait_ready(self.proc)
warmup()
@modal.exit()
def stop(self):
self.proc.terminate()
self.proc.wait()Deploy the server
To deploy the server on Modal, just run
modal deploy 06_gpu_and_ml/llm-serving/deepseek_v4_flash.pyThis will create a new App on Modal and build the container image for it if it hasn’t been built yet.
Test the server
To make it easier to test the server setup, we also include a local_entrypoint that hits the server with a simple client.
If you execute the command
modal run 06_gpu_and_ml/llm-serving/deepseek_v4_flash.pya fresh replica of the server will be spun up on Modal while the code below executes on your local machine.
This is akin to running simple tests inside of the if __name__ == "__main__" block of a Python script, but for cloud deployments!
@app.local_entrypoint()
def main(
prompt: str = "Explain why tech bros and climbers are an increasing phenomenon.",
):
url = Server.get_url()
print(f"server url: {url}")
wait_for_endpoint(url)
payload = {
"model": MODEL_NAME,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 1024,
"temperature": 0,
}
req = urllib.request.Request(
f"{url}/v1/chat/completions",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
print(f"sending a request to {url}")
with urllib.request.urlopen(req, timeout=STARTUP_TIMEOUT) as resp:
body = json.loads(resp.read())
message = body["choices"][0]["message"]
print(message.get("content"))
print(body.get("usage"))