Skip to content

Commit bb9f969

Browse files
Merge pull request #398 from HKU-BAL/test_gpu
added gpu calling workflow
2 parents 292b1ae + 1731295 commit bb9f969

18 files changed

+1273
-214
lines changed

.idea/Clai3_gpu_github.iml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile.gpu

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
FROM tensorflow/tensorflow:2.15.0-gpu
2+
3+
RUN apt-get update && apt-get install -y \
4+
wget \
5+
git \
6+
cmake \
7+
build-essential \
8+
automake \
9+
xz-utils \
10+
pigz \
11+
zlib1g-dev \
12+
libbz2-dev \
13+
liblzma-dev \
14+
libcurl4-openssl-dev \
15+
samtools \
16+
parallel \
17+
libboost-graph-dev \
18+
libssl-dev \
19+
libdeflate-dev \
20+
time \
21+
tabix \
22+
&& rm -rf /var/lib/apt/lists/*
23+
24+
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
25+
ENV PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/bin
26+
27+
WORKDIR /opt/bin
28+
29+
RUN pip3 install --no-cache-dir whatshap cffi
30+
31+
COPY . .
32+
33+
RUN cd /opt/bin/preprocess/realign && \
34+
g++ -std=c++14 -O1 -shared -fPIC -o realigner ssw_cpp.cpp ssw.c realigner.cpp && \
35+
g++ -std=c++11 -shared -fPIC -o debruijn_graph -O3 debruijn_graph.cpp && \
36+
wget http://www.bio8.cs.hku.hk/clair3/clair3_models/clair3_models.tar.gz -P /opt/models && \
37+
tar -zxvf /opt/models/clair3_models.tar.gz -C /opt/models && \
38+
rm /opt/models/clair3_models.tar.gz && \
39+
cd /opt/bin && make PREFIX=/usr/local PYTHON=/usr/bin/python3 && \
40+
rm -rf /opt/bin/samtools-* /opt/bin/longphase-* && \
41+
cd /opt/bin && wget -q https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux64.tar.bz2 && \
42+
tar -xjf pypy3.9-v7.3.8-linux64.tar.bz2 && \
43+
rm pypy3.9-v7.3.8-linux64.tar.bz2 && \
44+
ln -sf /opt/bin/pypy3.9-v7.3.8-linux64/bin/pypy3 /opt/bin/pypy3

Makefile

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,66 @@ ARCH := $(shell arch)
33

44
PYTHON ?= python3
55

6-
all : libhts.a longphase libclair3.so
6+
all : libhts.a longphase libclair3.so models
77
clean : clean_htslib clean_longphase clean_libclair3
88

99
SAMVER = 1.15.1
10-
LPVER = 1.7.3
11-
GCC ?= gcc
12-
GXX ?= g++
1310
PREFIX ?= ${CONDA_PREFIX}
1411
LDFLAGS = -L ${PREFIX}/lib
1512
CFLAGS = -fpic -std=c99 -O3 -I ${PREFIX}/include -L ${PREFIX}/lib
1613
CPPFLAGS = -std=c++11 -Wall -O3 -I ${PREFIX}/include -L ${PREFIX}/lib -Wl,-rpath=${PREFIX}/lib
1714

15+
ifeq ($(OS),Darwin)
16+
# Mac settings
17+
LPVER := 1.5
18+
CC_PATH ?= clang
19+
GCC ?= $(CC_PATH)
20+
else
21+
# Linux settings
22+
LPVER := 1.7.3
23+
GCC ?= gcc
24+
GXX ?= g++
25+
endif
26+
1827

1928
samtools-$(SAMVER)/Makefile:
20-
curl -L -o samtools-${SAMVER}.tar.bz2 https://github.com/samtools/samtools/releases/download/${SAMVER}/samtools-${SAMVER}.tar.bz2; \
21-
tar -xjf samtools-${SAMVER}.tar.bz2; \
22-
rm samtools-${SAMVER}.tar.bz2
29+
curl -L -o samtools-${SAMVER}.tar.bz2 https://github.com/samtools/samtools/releases/download/${SAMVER}/samtools-${SAMVER}.tar.bz2
30+
tar -xjf samtools-${SAMVER}.tar.bz2
31+
rm samtools-${SAMVER}.tar.bz2
2332

2433
libhts.a: samtools-$(SAMVER)/Makefile
2534
# this is required only to add in -fpic so we can build python module
2635
@echo "\x1b[1;33mMaking $(@F)\x1b[0m"
27-
cd samtools-${SAMVER}/htslib-${SAMVER}; CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" ./configure; make CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
36+
cd samtools-${SAMVER}/htslib-${SAMVER}; \
37+
if [ "$(OS)" = "Darwin" ]; then \
38+
autoheader && autoconf -Wno-syntax && ./configure; \
39+
else \
40+
CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}" ./configure; \
41+
fi; \
42+
make CFLAGS="${CFLAGS}" LDFLAGS="${LDFLAGS}"
2843
cp samtools-${SAMVER}/htslib-${SAMVER}/$@ $@
2944

3045

3146
longphase:
32-
curl -L -o longphase-${LPVER}.tar.xz https://github.com/twolinin/longphase/releases/download/v${LPVER}/longphase_linux-x64.tar.xz
33-
tar -xJf longphase-${LPVER}.tar.xz
34-
mv longphase_linux-x64 $@
35-
rm longphase-${LPVER}.tar.xz
36-
47+
if [ "$(OS)" = "Darwin" ]; then \
48+
curl -L -o v${LPVER}.tar.gz https://github.com/twolinin/longphase/archive/refs/tags/v${LPVER}.tar.gz; \
49+
tar -zxf v${LPVER}.tar.gz; \
50+
cd longphase-${LPVER} && export CC=${CC_PATH} && autoreconf -i && ./configure && make -j; \
51+
cd .. && rm v${LPVER}.tar.gz; \
52+
cp longphase-${LPVER}/longphase $@; \
53+
else \
54+
curl -L -o longphase-${LPVER}.tar.xz https://github.com/twolinin/longphase/releases/download/v${LPVER}/longphase_linux-x64.tar.xz; \
55+
tar -xJf longphase-${LPVER}.tar.xz; \
56+
mv longphase_linux-x64 $@; \
57+
rm longphase-${LPVER}.tar.xz; \
58+
fi
59+
60+
models:
61+
if [ "$(OS)" = "Darwin" ]; then \
62+
curl -L -o clair3_models.tar.gz http://www.bio8.cs.hku.hk/clair3/clair3_models/clair3_models.tar.gz; \
63+
mkdir -p ${PREFIX}/bin/models && tar -zxvf clair3_models.tar.gz -C ${PREFIX}/bin/models; \
64+
rm clair3_models.tar.gz; \
65+
fi
3766

3867
libclair3.so: samtools-${SAMVER}/htslib-${SAMVER} libhts.a
3968
${PYTHON} build.py
@@ -57,6 +86,9 @@ clean_htslib:
5786
.PHONY: clean_longphase
5887
clean_longphase:
5988
rm longphase
89+
if [ "$(OS)" = "Darwin" ]; then \
90+
rm -rf $(LONGPHASE_DIR); \
91+
fi
6092

6193
.PHONY: clean_libclair3
6294
clean_libclair3:

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ For somatic variant calling using **tumor-only** samples, please try [ClairS-TO]
6565
----
6666

6767
## Latest Updates
68+
*v1.2.0 (Aug 1, 2025)* : 1. Clair3 now natively supports GPU on Linux and Apple Silicon. Please refer to the [GPU quick start guide](docs/gpu_quick_start.md) for usage. Clair3 on GPU runs ~5 times compared CPU. Below is quick speed comparison.
69+
70+
<div align="center">
71+
<img src="docs/images/clair3_gpu_benchmark.png" width = "400" alt="Clair3 gpu benchmark">
72+
</div>
73+
6874
*v1.1.2 (Jul 10, 2025)* : 1. Added boundary check where an insertion is immediately followed by a soft-clipping ([#394](https://github.com/HKU-BAL/Clair3/issues/394), co-contributor @[Devon Ryan](https://github.com/dpryan79)) 2. Added exit code checking for all parallel jobs. The pipeline now immediately exits when encountering any job failure ([#392](https://github.com/HKU-BAL/Clair3/issues/392), co-contributor @[Sam Nicholls](https://github.com/SamStudio8)).
6975

7076
*v1.1.1 (May 19, 2025)* : 1. Fixed the malformed VCF header issue that occurred specifically in AWS cloud environments([#380](https://github.com/HKU-BAL/Clair3/issues/380)). 2.Added a Clair3 R10.4.1 model fine-tuned on 12 [bacterial genomes](https://elifesciences.org/reviewed-preprints/98300) with improved variant calling performance for bacterial samples. Performance benchmarks and detailed results are documented in our note ["fine-tuning_Clair3_with_12_bacteria_samples"](docs/fine-tuning_Clair3_with_12_bacteria_samples.pdf), (co-contributor @[William Shropshire](https://github.com/wshropshire)) .
@@ -204,6 +210,8 @@ Check the results using `less ${HOME}/clair3_ont_quickDemo/output/merge_output.v
204210

205211
## Installation
206212

213+
**Clair3 support GPU calling on Linux and Apple macOS (M1/M2/M3 chips) systems. Please refer the [GPU quick start](docs/gpu_quick_start.md) to install and run Clair3 with GPU.**
214+
207215
### Option 1. Docker pre-built image
208216

209217
A pre-built docker image is available [here](https://hub.docker.com/r/hkubal/clair3). With it you can run Clair3 using a single command.
@@ -364,11 +372,6 @@ docker build -f ./Dockerfile -t hkubal/clair3:latest .
364372
docker run -it hkubal/clair3:latest /opt/bin/run_clair3.sh --help
365373
```
366374

367-
368-
### Run Clair3 with Apple Silicon
369-
370-
Instructions are given as an answer to issue [#149](https://github.com/HKU-BAL/Clair3/issues/149).
371-
372375
----
373376

374377
## Usage

clair3.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"CallVarBam",
1212
"CallVariants",
1313
"Train",
14-
"CallVariantsFromCffi"
14+
"CallVariantsFromCffi",
15+
"CallVariantsFromCffiGPU",
1516
]
1617

1718
data_preprocess_folder = [
@@ -30,7 +31,7 @@
3031
'CheckEnvs',
3132
'SortVcf',
3233
'SelectQual',
33-
"CreateTensorPileupFromCffi"
34+
"CreateTensorPileupFromCffi",
3435
"CreateTensorFullAlignmentFromCffi",
3536
"CheckExitCode",
3637
]

clair3/CallVariants.py

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,36 @@ def Run(args):
221221
else:
222222
call_variants(args=args, output_config=output_config, output_utilities=output_utilities)
223223

224+
def print_debug_message(
225+
chromosome,
226+
position,
227+
gt21_probabilities,
228+
genotype_probabilities,
229+
variant_length_probabilities_1,
230+
variant_length_probabilities_2,
231+
extra_infomation_string=""
232+
):
233+
print("{}\t{}\t{}\t{}\t{}\t{}\t{}".format(
234+
chromosome,
235+
position,
236+
["{:0.8f}".format(x) for x in gt21_probabilities],
237+
["{:0.8f}".format(x) for x in genotype_probabilities],
238+
["{:0.8f}".format(x) for x in variant_length_probabilities_1],
239+
["{:0.8f}".format(x) for x in variant_length_probabilities_2],
240+
extra_infomation_string
241+
))
242+
243+
def gen_output_file():
244+
return
245+
246+
def close_opened_files():
247+
return
248+
def output_header(reference_file_path,
249+
cmd_fn,
250+
sample_name):
251+
header_str = get_header(reference_file_path=reference_file_path, cmd_fn=cmd_fn, sample_name=sample_name)
252+
return
253+
224254

225255
def output_utilties_from(
226256
sample_name,
@@ -231,52 +261,8 @@ def output_utilties_from(
231261
output_probabilities,
232262
cmd_fn=None,
233263
):
234-
def gen_output_file():
235-
global output_file
236-
if not output_probabilities:
237-
output_file = open(output_file_path, "w")
238-
239-
def output(string_value):
240-
global output_file
241-
string_value += '\n'
242-
output_file.write(string_value)
243-
244-
def print_debug_message(
245-
chromosome,
246-
position,
247-
gt21_probabilities,
248-
genotype_probabilities,
249-
variant_length_probabilities_1,
250-
variant_length_probabilities_2,
251-
extra_infomation_string=""
252-
):
253-
output("{}\t{}\t{}\t{}\t{}\t{}\t{}".format(
254-
chromosome,
255-
position,
256-
["{:0.8f}".format(x) for x in gt21_probabilities],
257-
["{:0.8f}".format(x) for x in genotype_probabilities],
258-
["{:0.8f}".format(x) for x in variant_length_probabilities_1],
259-
["{:0.8f}".format(x) for x in variant_length_probabilities_2],
260-
extra_infomation_string
261-
))
262-
263-
def close_opened_files():
264-
output_file.close()
265-
266-
def output_header():
267-
if is_output_for_ensemble:
268-
return
269-
270-
header_str = get_header(reference_file_path=reference_file_path, cmd_fn=cmd_fn, sample_name=sample_name)
271-
output(header_str)
272-
273-
return OutputUtilities(
274-
print_debug_message,
275-
output,
276-
output_header,
277-
close_opened_files,
278-
gen_output_file
279-
)
264+
#deprecated since v1.2.0
265+
return
280266

281267

282268
def homo_Ins_tuples_from(variant_length_probabilities_1, variant_length_probabilities_2, extra_probability):
@@ -1045,7 +1031,7 @@ def batch_output_for_ensemble(X, batch_chr_pos_seq, alt_info_list, batch_Y, outp
10451031
)
10461032

10471033

1048-
def batch_output(batch_chr_pos_seq, alt_info_list, batch_Y, output_config, output_utilities):
1034+
def batch_output(batch_chr_pos_seq, alt_info_list, batch_Y, output_config, output_utilities, args=None):
10491035
batch_size = len(batch_chr_pos_seq)
10501036

10511037
batch_gt21_probabilities, batch_genotype_probabilities = batch_Y[:,:param.label_shape_cum[0]], batch_Y[:,param.label_shape_cum[0]:param.label_shape_cum[1]]
@@ -1056,6 +1042,7 @@ def batch_output(batch_chr_pos_seq, alt_info_list, batch_Y, output_config, outpu
10561042
)
10571043
batch_variant_length_probabilities_1, batch_variant_length_probabilities_2 = [0] * batch_size, [0] * batch_size
10581044

1045+
batch_output_result = ""
10591046
if output_config.add_indel_length:
10601047
batch_variant_length_probabilities_1, batch_variant_length_probabilities_2 = batch_Y[:,param.label_shape_cum[1]:param.label_shape_cum[2]], batch_Y[:,param.label_shape_cum[2]:param.label_shape_cum[3]]
10611048
for (
@@ -1073,7 +1060,7 @@ def batch_output(batch_chr_pos_seq, alt_info_list, batch_Y, output_config, outpu
10731060
batch_variant_length_probabilities_1,
10741061
batch_variant_length_probabilities_2
10751062
):
1076-
output_with(
1063+
output_row = output_with(
10771064
chr_pos_seq,
10781065
alt_info,
10791066
gt21_probabilities,
@@ -1084,6 +1071,14 @@ def batch_output(batch_chr_pos_seq, alt_info_list, batch_Y, output_config, outpu
10841071
output_utilities,
10851072
)
10861073

1074+
if output_row is not None:
1075+
if args is not None:
1076+
args.output_file.write(output_row)
1077+
else:
1078+
#store the vcf output in batch in gpu mode
1079+
batch_output_result += output_row
1080+
1081+
return batch_output_result
10871082

10881083
def output_with(
10891084
chr_pos_seq,
@@ -1332,7 +1327,7 @@ def decode_alt_info(alt_info_dict, ref_base=None):
13321327
alternate_base)
13331328
PLs = ','.join([str(x) for x in PLs])
13341329

1335-
output_utilities.output("%s\t%d\t.\t%s\t%s\t%.2f\t%s\t%s\tGT:GQ:DP:AD:AF:PL\t%s:%d:%d:%s:%s:%s" % (
1330+
return "%s\t%d\t.\t%s\t%s\t%.2f\t%s\t%s\tGT:GQ:DP:AD:AF:PL\t%s:%d:%d:%s:%s:%s\n" % (
13361331
chromosome,
13371332
position,
13381333
reference_base,
@@ -1346,9 +1341,9 @@ def decode_alt_info(alt_info_dict, ref_base=None):
13461341
allele_depth,
13471342
allele_frequency_s,
13481343
PLs
1349-
))
1344+
)
13501345
else:
1351-
output_utilities.output("%s\t%d\t.\t%s\t%s\t%.2f\t%s\t%s\tGT:GQ:DP:AD:AF\t%s:%d:%d:%s:%s" % (
1346+
return "%s\t%d\t.\t%s\t%s\t%.2f\t%s\t%s\tGT:GQ:DP:AD:AF\t%s:%d:%d:%s:%s\n" % (
13521347
chromosome,
13531348
position,
13541349
reference_base,
@@ -1361,7 +1356,7 @@ def decode_alt_info(alt_info_dict, ref_base=None):
13611356
read_depth,
13621357
allele_depth,
13631358
allele_frequency_s,
1364-
))
1359+
)
13651360

13661361

13671362
def compute_PL(genotype_string, genotype_probabilities, gt21_probabilities, reference_base, alternate_base):
@@ -1443,8 +1438,11 @@ def call_variants(args, output_config, output_utilities):
14431438

14441439
m.load_weights(args.chkpnt_fn)
14451440

1446-
output_utilities.gen_output_file()
1447-
output_utilities.output_header()
1441+
args.output_file = open(args.call_fn, 'w') if args.call_fn != 'PIPE' else sys.stdout
1442+
header_str = get_header(reference_file_path=args.ref_fn, cmd_fn=args.cmd_fn, sample_name=args.sampleName)
1443+
header_str += '\n'
1444+
args.output_file.write(header_str)
1445+
14481446
chunk_id = args.chunk_id - 1 if args.chunk_id else None # 1-base to 0-base
14491447
chunk_num = args.chunk_num
14501448
full_alignment_mode = not args.pileup
@@ -1478,7 +1476,7 @@ def load_mini_batch():
14781476
total += len(X)
14791477
thread_pool.append(Thread(
14801478
target=batch_output_method,
1481-
args=(position, alt_info_list, prediction, output_config, output_utilities)
1479+
args=(position, alt_info_list, prediction, output_config, output_utilities, args)
14821480
))
14831481

14841482
if not is_finish_loaded_all_mini_batches:
@@ -1561,7 +1559,7 @@ def load_mini_batch():
15611559

15621560
logging.info("Total time elapsed: %.2f s" % (time() - variant_call_start_time))
15631561

1564-
output_utilities.close_opened_files()
1562+
args.output_file.close()
15651563
# remove file if on variant in output
15661564
if os.path.exists(args.call_fn):
15671565
for row in open(args.call_fn, 'r'):

0 commit comments

Comments
 (0)