Skip to content

Commit 676727f

Browse files
authored
ci: setup Jenkins (#874)
Co-authored-by: yongwww <[email protected]> The initial CI only includes a subset of unittests. We will further reduce the test time and add all tests in later PRs.
1 parent 8d29762 commit 676727f

32 files changed

+1866
-367
lines changed

Jenkinsfile

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// Jenkins pipeline
19+
// See documents at https://jenkins.io/doc/book/pipeline/jenkinsfile/
20+
21+
// Docker env used for testing
22+
// Different image may have different version tag
23+
// because some of them are more stable than anoter.
24+
//
25+
// Docker images are maintained by PMC, cached in dockerhub
26+
// and remains relatively stable over the time.
27+
// Flow for upgrading docker env(need commiter)
28+
//
29+
// - Send PR to upgrade build script in the repo
30+
// - Build the new docker image
31+
// - Tag the docker image with a new version and push to a binary cache.
32+
// - Update the version in the Jenkinsfile, send a PR
33+
// - Fix any issues wrt to the new image version in the PR
34+
// - Merge the PR and now we are in new version
35+
// - Tag the new version as the lates
36+
// - Periodically cleanup the old versions on local workers
37+
//
38+
39+
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
40+
// These are set at runtime from data in ci/jenkins/docker-images.yml, update
41+
// image tags in that file
42+
docker_run = "bash ci/bash.sh flashinfer/flashinfer-ci:latest"
43+
44+
def per_exec_ws(folder) {
45+
return "workspace/exec_${env.EXECUTOR_NUMBER}/" + folder
46+
}
47+
48+
def pack_lib(name, libs) {
49+
sh """
50+
echo "Packing ${libs} into ${name}"
51+
echo ${libs} | sed -e 's/,/ /g' | xargs md5sum
52+
"""
53+
stash includes: libs, name: name
54+
}
55+
56+
def unpack_lib(name, libs) {
57+
unstash name
58+
sh """
59+
echo "Unpacked ${libs} from ${name}"
60+
echo ${libs} | sed -e 's/,/ /g' | xargs md5sum
61+
"""
62+
}
63+
64+
def init_git(submodule = false) {
65+
cleanWs()
66+
// add retry in case checkout timeouts
67+
retry(5) {
68+
checkout scm
69+
}
70+
if (submodule) {
71+
retry(5) {
72+
timeout(time: 10, unit: 'MINUTES') {
73+
sh(script: 'git submodule update --init --recursive -f', label: 'Update git submodules')
74+
}
75+
}
76+
}
77+
}
78+
79+
// stage('Lint') {
80+
// node('CPU-SPOT') {
81+
// ws(per_exec_ws('flashinfer-lint')) {
82+
// init_git(false)
83+
// }
84+
// }
85+
// }
86+
87+
stage('JIT Unittest') {
88+
parallel(
89+
failFast: true,
90+
'GPU-G5-Test-1': {
91+
node('GPU-G5-SPOT') {
92+
ws(per_exec_ws('flashinfer-unittest')) {
93+
init_git(true) // we need cutlass submodule
94+
sh(script: "ls -alh", label: 'Show work directory')
95+
sh(script: "./scripts/task_show_node_info.sh", label: 'Show node info')
96+
sh(script: "${docker_run} ./scripts/task_jit_run_tests_part1.sh", label: 'JIT Unittest Part 1')
97+
}
98+
}
99+
},
100+
'GPU-G5-Test-2': {
101+
node('GPU-G5-SPOT') {
102+
ws(per_exec_ws('flashinfer-unittest')) {
103+
init_git(true) // we need cutlass submodule
104+
sh(script: "ls -alh", label: 'Show work directory')
105+
sh(script: "./scripts/task_show_node_info.sh", label: 'Show node info')
106+
sh(script: "${docker_run} ./scripts/task_jit_run_tests_part2.sh", label: 'JIT Unittest Part 2')
107+
}
108+
}
109+
}
110+
)
111+
}

ci/bash.sh

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
3+
if [ "$#" -lt 1 ]; then
4+
echo "Usage: ci/bash.sh <CONTAINER_NAME> -e key value -v key value [COMMAND]"
5+
exit -1
6+
fi
7+
8+
DOCKER_IMAGE_NAME=("$1")
9+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10+
WORKSPACE="$(pwd)"
11+
DOCKER_BINARY="docker"
12+
DOCKER_ENV="-e ENV_USER_ID=$(id -u) -e ENV_GROUP_ID=$(id -g)"
13+
DOCKER_VOLUMNS="-v ${WORKSPACE}:/workspace -v ${SCRIPT_DIR}:/docker"
14+
USE_GPU=true
15+
16+
shift 1
17+
while [[ $# -gt 0 ]]; do
18+
cmd="$1"
19+
if [[ $cmd == "-e" ]]; then
20+
env_key=$2
21+
env_value=$3
22+
shift 3
23+
DOCKER_ENV="${DOCKER_ENV} -e ${env_key}=${env_value}"
24+
elif [[ $cmd == "-v" ]]; then
25+
volumn_key=$2
26+
volumn_value=$3
27+
shift 3
28+
DOCKER_VOLUMNS="${DOCKER_VOLUMNS} -v ${volumn_key}:${volumn_value}"
29+
elif [[ $cmd == "-j" ]]; then
30+
num_threads=$2
31+
shift 2
32+
DOCKER_ENV="${DOCKER_ENV} -e NUM_THREADS=${num_threads} --cpus ${num_threads}"
33+
elif [[ $cmd == "--no-gpu" ]]; then
34+
USE_GPU=false
35+
shift
36+
else
37+
break
38+
fi
39+
done
40+
41+
if [ "$#" -eq 0 ]; then
42+
COMMAND="bash"
43+
if [[ $(uname) == "Darwin" ]]; then
44+
# Docker's host networking driver isn't supported on macOS.
45+
# Use default bridge network and expose port for jupyter notebook.
46+
DOCKER_EXTRA_PARAMS=("-it -p 8888:8888")
47+
else
48+
DOCKER_EXTRA_PARAMS=("-it --net=host")
49+
fi
50+
else
51+
COMMAND=("$@")
52+
fi
53+
54+
# Use nvidia-docker if the container is GPU.
55+
if [[ ${USE_GPU} == "true" ]]; then
56+
DOCKER_ENV="${DOCKER_ENV} -e CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES}"
57+
if type nvidia-docker 1> /dev/null 2> /dev/null; then
58+
DOCKER_BINARY=nvidia-docker
59+
else
60+
DOCKER_BINARY=docker
61+
DOCKER_ENV="${DOCKER_ENV} --gpus all"
62+
fi
63+
fi
64+
65+
# Print arguments.
66+
echo "DOCKER_BINARY ${DOCKER_BINARY}"
67+
echo "WORKSPACE: ${WORKSPACE}"
68+
echo "IMAGE NAME: ${DOCKER_IMAGE_NAME}"
69+
echo "ENV VARIABLES: ${DOCKER_ENV}"
70+
echo "VOLUMES: ${DOCKER_VOLUMNS}"
71+
echo "COMMANDS: '${COMMAND[@]}'"
72+
73+
# By default we cleanup - remove the container once it finish running (--rm)
74+
# and share the PID namespace (--pid=host) so the process inside does not have
75+
# pid 1 and SIGKILL is propagated to the process inside (jenkins can kill it).
76+
77+
${DOCKER_BINARY} run --rm --pid=host \
78+
-w /workspace \
79+
${DOCKER_VOLUMNS} \
80+
${DOCKER_ENV} \
81+
${DOCKER_EXTRA_PARAMS[@]} \
82+
${DOCKER_IMAGE_NAME} \
83+
${COMMAND[@]}

ci/scripts/jenkins/git_skip_ci.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
import argparse
20+
import logging
21+
import os
22+
23+
from cmd_utils import init_log, tags_from_title
24+
from git_utils import GitHubRepo, git, parse_remote
25+
26+
if __name__ == "__main__":
27+
help = "Exits with 0 if CI should be skipped, 1 otherwise"
28+
parser = argparse.ArgumentParser(description=help)
29+
parser.add_argument("--pr", required=True)
30+
parser.add_argument("--remote", default="origin", help="ssh remote to parse")
31+
parser.add_argument(
32+
"--pr-title", help="(testing) PR title to use instead of fetching from GitHub"
33+
)
34+
args = parser.parse_args()
35+
init_log()
36+
37+
branch = git(["rev-parse", "--abbrev-ref", "HEAD"])
38+
log = git(["log", "--format=%s", "-1"])
39+
40+
# Check the PR's title (don't check this until everything else passes first)
41+
def check_pr_title():
42+
remote = git(["config", "--get", f"remote.{args.remote}.url"])
43+
user, repo = parse_remote(remote)
44+
45+
if args.pr_title:
46+
title = args.pr_title
47+
else:
48+
github = GitHubRepo(token=os.environ["GITHUB_TOKEN"], user=user, repo=repo)
49+
pr = github.get(f"pulls/{args.pr}")
50+
title = pr["title"]
51+
logging.info(f"pr title: {title}")
52+
tags = tags_from_title(title)
53+
logging.info(f"Found title tags: {tags}")
54+
return "skip ci" in tags
55+
56+
if (
57+
args.pr != "null"
58+
and args.pr.strip() != ""
59+
and branch != "main"
60+
and check_pr_title()
61+
):
62+
logging.info("PR title starts with '[skip ci]', skipping...")
63+
exit(0)
64+
else:
65+
logging.info(
66+
f"Not skipping CI:\nargs.pr: {args.pr}\nbranch: {branch}\ncommit: {log}"
67+
)
68+
exit(1)
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
19+
import argparse
20+
import fnmatch
21+
from typing import Optional
22+
23+
from git_utils import git
24+
25+
globs = [
26+
"*.md",
27+
".github/*",
28+
"docs/*",
29+
".gitignore",
30+
"LICENSE",
31+
"tests/lint/*",
32+
"tests/scripts/task_lint.sh",
33+
]
34+
35+
36+
def match_any(f: str) -> Optional[str]:
37+
for glob in globs:
38+
if fnmatch.fnmatch(f, glob):
39+
return glob
40+
return None
41+
42+
43+
if __name__ == "__main__":
44+
help = "Exits with code 1 if a change only touched files, indicating that CI could be skipped for this changeset"
45+
parser = argparse.ArgumentParser(description=help)
46+
parser.add_argument(
47+
"--files", help="(testing only) comma separated list of files to check"
48+
)
49+
args = parser.parse_args()
50+
print(args)
51+
if args.files is not None:
52+
diff = [x for x in args.files.split(",") if x.strip() != ""]
53+
else:
54+
diff = git(["diff", "--no-commit-id", "--name-only", "-r", "origin/main"])
55+
diff = diff.split("\n")
56+
diff = [d.strip() for d in diff]
57+
diff = [d for d in diff if d != ""]
58+
59+
print(f"Changed files:\n{diff}")
60+
61+
if len(diff) == 0:
62+
print("Found no changed files, skipping CI")
63+
exit(0)
64+
65+
print(f"Checking with globs:\n{globs}")
66+
67+
for file in diff:
68+
match = match_any(file)
69+
if match is None:
70+
print(f"{file} did not match any globs, running CI")
71+
exit(1)
72+
else:
73+
print(f"{file} matched glob {match}")
74+
75+
print("All files matched a glob, skipping CI")
76+
exit(0)

0 commit comments

Comments
 (0)