From 42bbcb15275793ce6ecce01adc0a6754a5459232 Mon Sep 17 00:00:00 2001 From: Yibing Liu Date: Tue, 4 Feb 2020 14:30:57 +0800 Subject: [PATCH] Release pantheon (#56) * Init pantheon * Update README * Fix pantheon import * Update README * Fix the possible bug when del student * Format docs of public methods * Add api guide & docs for pantheon * Use str2bool instead of bool --- README.md | 12 +- demo/pantheon/README.md | 2 + demo/pantheon/run_student.py | 103 ++++ demo/pantheon/run_teacher1.py | 80 +++ demo/pantheon/run_teacher2.py | 79 +++ demo/pantheon/utils.py | 91 ++++ docs/docs/api/api_guide.md | 2 + docs/docs/api/pantheon_api.md | 256 ++++++++++ docs/mkdocs.yml | 3 +- paddleslim/pantheon/README.md | 252 ++++++++++ paddleslim/pantheon/__init__.py | 4 + paddleslim/pantheon/images/pantheon_arch.png | Bin 0 -> 98589 bytes paddleslim/pantheon/student.py | 484 ++++++++++++++++++ paddleslim/pantheon/teacher.py | 501 +++++++++++++++++++ paddleslim/pantheon/utils.py | 61 +++ 15 files changed, 1926 insertions(+), 4 deletions(-) create mode 100644 demo/pantheon/README.md create mode 100644 demo/pantheon/run_student.py create mode 100644 demo/pantheon/run_teacher1.py create mode 100644 demo/pantheon/run_teacher2.py create mode 100644 demo/pantheon/utils.py create mode 100644 docs/docs/api/pantheon_api.md create mode 100644 paddleslim/pantheon/README.md create mode 100644 paddleslim/pantheon/__init__.py create mode 100644 paddleslim/pantheon/images/pantheon_arch.png create mode 100644 paddleslim/pantheon/student.py create mode 100644 paddleslim/pantheon/teacher.py create mode 100644 paddleslim/pantheon/utils.py diff --git a/README.md b/README.md index 87f881b1..9358d3b3 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,17 @@ We encapsulate each compression and search method to a compression strategy clas ### Knowledge Distillation -- PaddleSlim supports the following losses added on any paired layers between teacher and student models: - - Flow of the solution procedure (FSP) loss. - - L2 loss. +- **Naive knowledge distillation**: transfers dark knowledge by merging the teacher and student model into the same Program, and supports the following losses added on any paired layers between teacher and student models: + - Flow of the solution procedure (FSP) loss; + - L2 loss; - Softmax with cross-entropy loss. +- **Paddle large-scale scalable knowledge distillation framework [Pantheon](paddleslim/pantheon)**: a universal solution for knowledge distillation, more flexible than the naive knowledge distillation, and easier to scale to the large-scale applications. + - Decouple the teacher and student models --- they run in different processes in the same or different nodes, and transfer knowledge via TCP/IP ports or local files; + - Friendly to assemble multiple teacher models and each of them can work in either online or offline mode independently; + - Merge knowledge from different teachers and make batch data for the student model automatically; + - Support the large-scale knowledge prediction of teacher models on multiple devices. + ### Lightweight Network Architecture Search (Light-NAS) - PaddleSlim provides Simulated Annealing (SA)-based lightweight network architecture search method. diff --git a/demo/pantheon/README.md b/demo/pantheon/README.md new file mode 100644 index 00000000..3cc55c33 --- /dev/null +++ b/demo/pantheon/README.md @@ -0,0 +1,2 @@ + +The toy examples for Pantheon, see details in [PaddleSlim/Pantheon](../../paddleslim/pantheon). diff --git a/demo/pantheon/run_student.py b/demo/pantheon/run_student.py new file mode 100644 index 00000000..19d9b206 --- /dev/null +++ b/demo/pantheon/run_student.py @@ -0,0 +1,103 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed 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 argparse +from paddleslim.pantheon import Student + +from utils import str2bool + + +def parse_args(): + parser = argparse.ArgumentParser(__doc__) + parser.add_argument( + "--in_address0", + type=str, + default=None, + help="Input address for teacher 0. (default: %(default)s)") + parser.add_argument( + "--in_path0", + type=str, + default=None, + help="Input file path for teacher 0. (default: %(default)s)") + parser.add_argument( + "--in_address1", + type=str, + default=None, + help="Input address for teacher 1. (default: %(default)s)") + parser.add_argument( + "--in_path1", + type=str, + default=None, + help="Input file path for teacher 1. (default: %(default)s)") + parser.add_argument( + "--test_send_recv", + type=str2bool, + default=False, + help="Whether to test send/recv interfaces. (default: %(default)s)") + parser.add_argument( + "--batch_size", + type=int, + default=32, + help="The batch size of student model. (default: %(default)s)") + args = parser.parse_args() + return args + + +def run(args): + if args.in_address0 and args.in_path0: + raise ValueError( + "args.in_address0 and args.in_path0 should not be valid " + "at the same time!") + if not args.in_address0 and not args.in_path0: + raise ValueError( + "One of args.in_address0 and args.in_path0 must be valid!") + + if args.in_address1 and args.in_path1: + raise ValueError( + "args.in_address1 and args.in_path1 should not be valid " + "at the same time!") + if not args.in_address1 and not args.in_path1: + raise ValueError( + "One of args.in_address1 and args.in_path1 must be valid") + + student = Student(merge_strategy={"result": "sum"}) + + student.register_teacher( + in_address=args.in_address0, in_path=args.in_path0) + student.register_teacher( + in_address=args.in_address1, in_path=args.in_path1) + student.start() + + if args.test_send_recv: + for t in xrange(2): + for i in xrange(3): + print(student.recv(t)) + student.send("message from student!") + + knowledge_desc = student.get_knowledge_desc() + data_generator = student.get_knowledge_generator( + batch_size=args.batch_size, drop_last=False) + for batch_data in data_generator(): + batch_size = list(batch_data.values())[0].shape[0] + keys = batch_data.keys() + for i in range(batch_size): + data = {} + for key in keys: + data[key] = batch_data[key][i] + print(data) + + +if __name__ == '__main__': + args = parse_args() + run(args) diff --git a/demo/pantheon/run_teacher1.py b/demo/pantheon/run_teacher1.py new file mode 100644 index 00000000..bbe94310 --- /dev/null +++ b/demo/pantheon/run_teacher1.py @@ -0,0 +1,80 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed 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 numpy as np +import paddle.fluid as fluid + +from utils import parse_args, sample_generator, sample_list_generator, batch_generator +from paddleslim.pantheon import Teacher + + +def run(args): + if args.out_path and args.out_port: + raise ValueError("args.out_path and args.out_port should not be valid " + "at the same time") + if not args.out_path and not args.out_port: + raise ValueError("One of args.out_path and args.out_port be valid") + + # user-defined program: y = 2*x - 1 + startup = fluid.Program() + program = fluid.Program() + with fluid.program_guard(program, startup): + inp_x = fluid.layers.data(name='x', shape=[-1, 1], dtype="int64") + y = inp_x * 2 - 1 + result = fluid.layers.assign(y) + + place = fluid.CUDAPlace(0) if args.use_cuda else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(startup) + + teacher = Teacher(out_path=args.out_path, out_port=args.out_port) + teacher.start() + + if args.generator_type == "sample_generator": + reader_config = { + "sample_generator": sample_generator(max_n=1000), + "batch_size": args.batch_size, + "drop_last": False + } + elif args.generator_type == "sample_list_generator": + reader_config = { + "sample_list_generator": sample_list_generator( + max_n=1000, batch_size=args.batch_size) + } + else: + reader_config = { + "batch_generator": batch_generator( + max_n=1000, batch_size=args.batch_size) + } + + if args.test_send_recv: + teacher.send("greetings from teacher1") + teacher.send({"x": 1, "y": 2}) + teacher.send({3, 5}) + print("recved {}".format(teacher.recv())) + + teacher.start_knowledge_service( + feed_list=[inp_x.name], + schema={"x": inp_x, + "2x-1": y, + "result": result}, + program=program, + reader_config=reader_config, + exe=exe, + times=args.serving_times) + + +if __name__ == '__main__': + args = parse_args() + run(args) diff --git a/demo/pantheon/run_teacher2.py b/demo/pantheon/run_teacher2.py new file mode 100644 index 00000000..5d45fec9 --- /dev/null +++ b/demo/pantheon/run_teacher2.py @@ -0,0 +1,79 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed 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 numpy as np +import paddle.fluid as fluid + +from utils import parse_args, sample_generator, sample_list_generator, batch_generator +from paddleslim.pantheon import Teacher + + +def run(args): + if args.out_path and args.out_port: + raise ValueError("args.out_path and args.out_port should not be valid " + "at the same time") + if not args.out_path and not args.out_port: + raise ValueError("One of args.out_path and args.out_port be valid") + + # user-defined program: y = 2*x + 1 + startup = fluid.Program() + program = fluid.Program() + with fluid.program_guard(program, startup): + inp_x = fluid.layers.data(name='x', shape=[-1, 1], dtype="int64") + y = inp_x * 2 + 1 + result = fluid.layers.assign(y) + + place = fluid.CUDAPlace(0) if args.use_cuda else fluid.CPUPlace() + exe = fluid.Executor(place) + exe.run(startup) + + teacher = Teacher(out_path=args.out_path, out_port=args.out_port) + teacher.start() + + if args.generator_type == "sample_generator": + reader_config = { + "sample_generator": sample_generator(max_n=1000), + "batch_size": args.batch_size, + "drop_last": False + } + elif args.generator_type == "sample_list_generator": + reader_config = { + "sample_list_generator": sample_list_generator( + max_n=1000, batch_size=args.batch_size) + } + else: + reader_config = { + "batch_generator": batch_generator( + max_n=1000, batch_size=args.batch_size) + } + + if args.test_send_recv: + teacher.send("greetings from teacher2") + teacher.send([1]) + teacher.send({1, 2, 3}) + print("recved {}".format(teacher.recv())) + + teacher.start_knowledge_service( + feed_list=[inp_x.name], + schema={"2x+1": y, + "result": result}, + program=program, + reader_config=reader_config, + exe=exe, + times=args.serving_times) + + +if __name__ == '__main__': + args = parse_args() + run(args) diff --git a/demo/pantheon/utils.py b/demo/pantheon/utils.py new file mode 100644 index 00000000..af88d2a6 --- /dev/null +++ b/demo/pantheon/utils.py @@ -0,0 +1,91 @@ +import numpy as np +import argparse + + +def str2bool(v): + return v.lower() in ("true", "t", "1") + + +def parse_args(): + parser = argparse.ArgumentParser(__doc__) + parser.add_argument( + "--out_port", + type=int, + default=None, + help="IP port number for sending out data. (default: %(default)s)") + parser.add_argument( + "--out_path", + type=str, + default=None, + help="The file path to dump knowledge data. (default: %(default)s)") + parser.add_argument( + "--use_cuda", + type=str2bool, + default=False, + help="Whether to use GPU for prediction. (default: %(default)s)") + parser.add_argument( + "--test_send_recv", + type=str2bool, + default=False, + help="Whether to test send/recv interfaces. (default: %(default)s)") + parser.add_argument( + "--generator_type", + type=str, + choices=[ + "sample_generator", "sample_list_generator", "batch_generator" + ], + default="batch_generator", + help="Which data generator to use. (default: %(default)s)") + parser.add_argument( + "--batch_size", + type=int, + default=32, + help="The batch size per device for data generators. (default: %(default)s)" + ) + parser.add_argument( + "--serving_times", + type=int, + default=1, + help="The maximum times of teacher serving knowledge. (default: %(default)s)" + ) + args = parser.parse_args() + return args + + +def sample_generator(max_n): + def wrapper(): + for i in range(max_n): + yield [i] + + return wrapper + + +def sample_list_generator(max_n, batch_size=500): + def wrapper(): + sample_list = [] + for sample in sample_generator(max_n)(): + if len(sample_list) < batch_size: + sample_list.append(sample) + if len(sample_list) == batch_size: + yield sample_list + sample_list = [] + if len(sample_list) > 0: + yield sample_list + + return wrapper + + +# data_generator +def batch_generator(max_n, batch_size=500): + def wrapper(): + batch = [] + for sample in sample_generator(max_n)(): + if len(batch) < batch_size: + batch.append(sample) + if len(batch) == batch_size: + yield [np.array(batch).astype('int64').reshape((-1, 1))] + batch = [] + if len(batch) > 0: + yield [np.array(batch).astype('int64').reshape((-1, 1))] + + return wrapper diff --git a/docs/docs/api/api_guide.md b/docs/docs/api/api_guide.md index 79910a06..650bfc3b 100644 --- a/docs/docs/api/api_guide.md +++ b/docs/docs/api/api_guide.md @@ -8,6 +8,8 @@ - [单进程蒸馏](./single_distiller_api.md) +- [大规模可扩展知识蒸馏框架 Pantheon](./pantheon_api.md) + - [通道剪裁](./prune_api.md) ### [量化](./quantization_api.md) diff --git a/docs/docs/api/pantheon_api.md b/docs/docs/api/pantheon_api.md new file mode 100644 index 00000000..b78c5069 --- /dev/null +++ b/docs/docs/api/pantheon_api.md @@ -0,0 +1,256 @@ +## Teacher + +pantheon.Teacher()[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/teacher.py#L78) + +: The class defined for the teacher model. Generate knowledge data and transfer them to the student model. + +**Args:** + +- **out\_path (str|None)** - The path to dump knowledge data for offline mode. + +- **out\_port (int|None)** - The IP port number to send out knowledge for online mode, should be unique when launching multiple teachers in the same node. + +**Return:** An object of class Teacher + + +pantheon.Teacher.start()[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/teacher.py#L133) + +: Start teacher service, sychronize with student and launch the thread + to monitor commands from student. + +**Args:** None + +**Return:** None + + +pantheon.Teacher.send(data)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/teacher.py#L181) + +: Send one data object to student. + +**Args:** + +- **data (Python data):** - The data to be sent, can be any type of Python data object. + +**Return:** None + + +pantheon.Teacher.recv()[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/teacher.py#L196) + +: Recieve one data object from student. + +**Args:** None + +**Return:** + +- The received data, can be any type of Python data object. + + +pantheon.Teacher.dump(knowledge)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/teacher.py#L214) + +: Dump one batch knowledge data into the output file, only used in the offline mode. + +**Args:** + +- **knowledge (dict):** - The knowledge data to be dumped. + +**Return:** None + + +pantheon.Teacher.start\_knowledge\_service(feed\_list, schema, program, reader\_config, exe, buf\_size=10, times=1)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/teacher.py#L259) + +: Start the knowledge service to generate and transfer knowledge data. In GPU mode, the devices to execute knowledge prediction will be determined by the + environment variable **FLAGS\_selected\_gpus**, or by **CUDA\_VISIBLE\_DEVICES** if it is not set, and by **CPU\_NUM** (default 1) in CPU mode. Only supported in static graph. + + **Args:** + + - **feed\_list (list):** - A list of feed Variables or their names for the + input teacher Program. + - **schema (dict):** - A dictionary to specify keys and fetched Variables + to generate knowledge. + - **program (fluid.Program):** - Inference Program of the teacher model. + - **reader\_config (dict):** - The config for data reader. Support all the three types of generators used by [fluid.io.PyReader](https://www.paddlepaddle.org.cn/documentation/docs/en/api/io/PyReader.html) and [fluid.io.DataLoader](https://www.paddlepaddle.org.cn/documentation/docs/en/api/io/DataLoader.html#dataloader), and their configs contain the key-value pair of the generator type and a generator object, plus other necessary argument pairs. See the following: + + - 1) sample generator: + + reader\_config={"sample\_generator": #some\_sample\_generator, + "batch\_size": #batch\_size, "drop\_last": #drop\_last}, + + 'drop\_last' set to True by default, + - 2) sample list generator: + + reader\_config={"sample\_list\_generator": #some\_sample\_list\_generator}, + - 3) batch generator: + + reader\_config={"batch\_generator": #some\_batch\_genrator}. + + The trial to parse config will be in the order of 1) -> 3), and any other unrelated keys in these configs will be ignored. + +- **exe (fluid.Executor):** The executor to run the input program. +- **buf\_size (int):** The size of buffers for data reader and knowledge + writer on each device. +- **times (int):** The maximum repeated serving times, default 1. Whenever + the public method **get\_knowledge\_generator()** in Student + object called once, the serving times will be added one, + until reaching the maximum and ending the service. + +**Return:** None + +**Examples:** + +Note: this example should be run with the example of class **Student**. + +```python +import paddle +import paddle.fluid as fluid +from paddleslim.pantheon import Teacher + +startup = fluid.Program() +program = fluid.Program() +with fluid.program_guard(program, startup): + images = fluid.data( + name='pixel', shape=[None, 3 * 32 * 32], dtype='float32') + labels = fluid.data(name='label', shape=[None, 1], dtype='int64') + logits = fluid.layers.fc(input=images, size=10) + loss = fluid.layers.softmax_with_cross_entropy(logits, labels) + +place = fluid.CPUPlace() +exe = fluid.Executor(place) +exe.run(startup) + +train_reader = paddle.batch( + paddle.dataset.cifar.train10(), batch_size=32) + +teacher = Teacher(out_path="example_knowledge.dat", # offline mode + #out_port=5000 # online mode + ) +teacher.start() + +teacher.start_knowledge_service( + feed_list=[images, labels], + schema={"logits": logits, + "labels": labels}, + program=program, + reader_config={"sample_list_generator": train_reader}, + exe=exe) +``` + + +## Student + +pantheon.Student(merge_strategy=None)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L34) + +: The class defined for the student model. Receive knowledge data from + teacher model and carry out knowledge merging. + + **Args:** + + - **merge\_strategy (dict|None):** - A dictionary whose keys are the common schemas shared by different teachers, and each corresponding value specifies the merging strategy for different schemas respectively, supporting **sum** and **mean** now. + +**Return:** An object of class Student. + + +pantheon.Student.register\_teacher(in\_path=None, in\_address=None)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L72) + +: Register one teacher model and assign the order number to it as its id, with the file path (offline mode) or IP address (online mode) that the teacher model writes knowledge data to. + +**Args:** + +- **in\_path (str|None):** The input file path. Default None. +- **in\_address (str|None):** The input IP address, in the format "\:\" (e.g. "127.0.0.1:8080"). Default None. + +**Return:** None + + +pantheon.Student.start()[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L213) + +: End teachers' registration and synchronize with all of them. + +**Args:** None + +**Return:** None + +pantheon.Student.send(self, data, teacher_ids=None)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L240) + +: Send data to teachers. + +**Args:** + +- **data (Python data):** - A Python data object to be sent. +- **teacher_ids (list|None):** - A list of teacher ids to send data. If set to None, send the data to all teachers. Default None. + +**Return:** None + +pantheon.Student.recv(teacher_id)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L262) + +: Receive data from one teacher. + + **Args:** + +- **teacher\_id (int):** - The id of teacher that receives data from. + +**Return:** + +- The received data object. + +pantheon.Student.get\_knowledge\_desc()[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L283) + + : Get description for knowledge, including shape, data type and lod level for each schema. + + **Args:** None + + **Return:** + + - Knowledge description, which is a dict. + + +pantheon.Student.get\_knowledge\_qsize()[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L318) + + : Get the real-time size of knowledge queue. If this size is denoted as + **qsize**, it means that there are **qsize** batch knowledge data + already pushed into knowledge queue and waiting for the knowledge + generator to pop out. It's dynamic and limited up to 100, the capacity + of the knowledge queue. + + **Args:** None + + **Return:** + + - The real-time size of knowledge queue. + +pantheon.Student.get\_knowledge\_generator(batch\_size, drop\_last=False)[source code](https://github.com/PaddlePaddle/PaddleSlim/blob/develop/paddleslim/pantheon/student.py#L334) + +: Get the generator for knowledge data, return None if last generator doesn't finish yet. + +**Args:** + +- **batch\_size (int):** - The batch size of returned knowledge data. +- **drop\_last (bool):** - Whether to drop the last batch if its size is less than batch size. + +**Return:** + +- The wrapper of knowledge data generator. + +**Examples:** + +Note: this example should be run with the example of class **Teacher**. + +```python +from paddleslim.pantheon import Student + +student = Student() + +student.register_teacher(in_path="example_knowledge.dat", # offline mode + #in_address="127.0.0.1:5000" # online mode + ) +student.start() + +knowledge_desc = student.get_knowledge_desc() +data_generator = student.get_knowledge_generator( + batch_size=128, drop_last=True) + +# get knowledge data +for knowledge in data_generator(): + print("knowledge queue size: {}".format(student.get_knowledge_qsize())) + + # do something else +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index d5f725a8..ad81dea7 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -16,7 +16,8 @@ nav: - 量化: api/quantization_api.md - 剪枝与敏感度: api/prune_api.md - 模型分析: api/analysis_api.md - - 知识蒸馏: api/single_distiller_api.md + - 简单知识蒸馏: api/single_distiller_api.md + - 大规模可扩展知识蒸馏框架 Pantheon: api/pantheon_api.md - SA搜索: api/nas_api.md - One-shot搜索: api/one_shot_api.md - 搜索空间: search_space.md diff --git a/paddleslim/pantheon/README.md b/paddleslim/pantheon/README.md new file mode 100644 index 00000000..a9f564dc --- /dev/null +++ b/paddleslim/pantheon/README.md @@ -0,0 +1,252 @@ +# Pantheon: Paddle large-scale scalable knowledge distillation framework + +Pantheon is a universal solution for knowledge distillation in Paddle Fluid. Its design takes account of many possible behaviors of teacher models. Every teacher and student model in Pantheon works in different processes and they communicate with each other via local files or TCP/IP ports. The knowledge can be easily transferred to the student model from a single teacher model or the ensemble of multiple teacher models, in which each teacher model can work in online or offline mode independently. And Pantheon also provides a highly optimized interface for the large-scale prediction of teacher models. Beneficial from the low coupling of teachers and the student, users can allocate computation resources for different roles dependent on their computation complexity, and build a large-scale and practical knowledge distillation learning system on Pantheon. + +The illustration below shows an application of Pantheon, where the sudent model is trained with knowledge from multiple online teachers. These teachers may work on the same node but different devices, or different nodes with the student model, as long as they can communicate with each other via the Internet. The student model can send queries to teachers, and the latter take these queries as input and generate streaming knowledge data for the former. Or in a simpler way, the student model can read the training data in the **same order** with the teachers, avoiding the procedure of sending queryies. + + +
+
+ The architecture for one online knowledge distillation system based on Pantheon +
+ +## Prerequisites + +- Python 2.7.x or 3.x +- PaddlePaddle >= 1.6.0 + +## APIs + +Pantheon defines two classes **Teacher** and **Student** for the communication and knowledge transfer between teacher and student. + +- **Teacher**: used by the teacher model. Can receive queries from student and write out the knowledge from teacher model via TCP/IP port (online mode) or into a local file (offline mode). +- **Student**: used by the student model. Can receive and merge the knowledge from teachers, and feed the student model along with local data for training. + +Usually, the public methods of these two classes work in the pairwise way. Their mapping relations and suitable working modes are listed in the following table. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TeacherStudentSupported GraphModeremarks
staticdynamiconlineoffline
__init__(
    out_path=None,
    out_port=None)
__init__(
    merge_strategy=None)
[1]
register_teacher( +
    in_path=None, +
    in_address=None) +
[2]
start()start()
[3]
send(data)recv(teacher_id)
[4]
recv()send(data,
    +  teacher_ids=None) +
[5]
dump(knowledge)
[6]
start_knowledge_service( +
    feed_list, +
    schema, +
    program, +
    reader_config, +
    exe, +
    buf_size=10, +
    times=1)
get_knowledge_desc()
[7]
get_knowledge_qsize()
get_knowledge_generator(
    batch_size, +
    drop_last=False)
+ +**Remarks:** + + - [1] Decalre the teacher object for teacher model with **out\_path** or **out\_port**, and the student for student model with **merge\_strategy** for knowledge from different teachers. + - [2] Register a teacher, and allocate an id for it which starts from zero in the order of registration. **register\_teacher()** can be called many times for multiple-teacher mode. + - [3] Estabish TCP/IP link between teachers and the student, and synchronize all of them. + - [4] Send one data from teacher to student. + - [5] Send one data from student to teacher. + - [6] Dump one batch knowledge data into the output file. + - [7] Highly optimized high-level interfaces to build service for knowledge transfer: + - **start\_knowledge\_service()** can perform large-scale prediction of teacher model on multiple devices; + - Support auto merging of knowledge from different teachers; + - Support auto reconnection of student and teachers. + +### About the data format + +- **Knowledge**: A dictionary with the keys specified by users and the values that are numpy ndarray tensors predicted by teacher models. The first dimension of tensors should be batch size and LoDTensor is not supported yet. One can call **get\_knowledge\_desc()** to get the description of knowledge, which is also a dictionary, including the shape, data type and LoD level about knowledge data. +- **Offline knowledge file**: The first line is knowledge description, and the following lines are knowledge data, one line for one batch samples, all dumped by cPickle. + + + +### Usage + +If separately runnable teacher models and the student model +have been ready, basically one can build the trainable system with knowledge +distillation by following two simple steps. + +1) Instantiate a **Teacher** object for the teacher model, and launch knowledge serving + +```python + +from paddleslim.pantheon import Teacher +... + +teacher = Teacher(out_path=args.out_path, out_port=args.out_port) +teacher.start() + +teacher.start_knowledge_service( + feed_list=[inp_x.name], + schema={"x": inp_x, + "y": y}, + program=program, + reader_config={"batch_generator": batch_generator}, + exe=exe, + buf_size=100, + times=1) +``` + +2) Instantiate a **Student** object, specify the way to merge knowledge, register teachers, + and get knowledge description and data generator for the student model + +```python +from paddleslim.pantheon import Student +... + +student = Student(merge_strategy={"result": "sum"}) + +student.register_teacher( + in_address=args.in_address0, in_path=args.in_path0) +student.register_teacher( + in_address=args.in_address1, in_path=args.in_path1) +student.start() + +knowledge_desc = student.get_knowledge_desc() +data_generator = student.get_knowledge_generator( + batch_size=32, drop_last=False) +``` + +### Example + +Here provide a toy example to show how the knowledge data is transferred from teachers to the student model and merged. + +In the directory [demo/pantheon/](../../demo/pantheon/), there implement two teacher models (not trainable, just for demo): teacher1 takes an integer **x** as input and predicts value **2x-1**, see in [run_teacher1.py](../../demo/pantheon/run_teacher1.py); teacher2 also takes **x** as input and predicts **2x+1**, see in [run_teacher2.py](../../demo/pantheon/run_teacher2.py). They two share a data reader to read a sequence of increasing natural numbers from zero to some positive inter **max_n** as input and generate different knowledge. And the schema keys for knowledge in teacher1 is [**"x", "2x-1", "result"**], and [**"2x+1", "result"**] for knowledge in teacher2, in which **"result"** is the common schema and the copy of two predictions respectively. On instantiating the **Student** object, the merging strategy for the common schema **"result"** should be specified, and the schema keys for the merged knowledge will be [**"x", "2x-1", "2x+1", "result"**], with the merged **"result"** equal to **"2x"** when the merging strategy is **"mean"** and **"4x"** when merging strategy is **"sum"**. The student model gets merged knowledge from teachers and prints them out, see in [run_student.py](../../demo/pantheon/run_student.py). + +The toy "knowledge distillation" system can be launched in three different modes, i.e., offline, online and their hybrid. All three modes should have the same outputs, and the correctness of results can be verified by checking the order and values of outputs. + +1) **Offline** + + The two teachers work in offline mode, and start them with given local file paths. + + ```shell +export PYTHONPATH=../../:$PYTHONPATH +export CUDA_VISIBLE_DEVICES=0,1 +nohup python -u run_teacher1.py --use_cuda true --out_path teacher1_offline.dat > teacher1_offline.log 2>&1& +export CUDA_VISIBLE_DEVICES=2 +nohup python -u run_teacher2.py --use_cuda true --out_path teacher2_offline.dat > teacher2_offline.log 2>&1& + ``` + After the two executions both finished, start the student model with the two generated knowledge files. + + ```shell +export PYTHONPATH=../../:$PYTHONPATH + python -u run_student.py \ + --in_path0 teacher1_offline.dat \ + --in_path1 teacher2_offline.dat + ``` + + +2) **Online** + +The two teachers work in online mode, and start them with given TCP/IP ports. Please make sure that the ICP/IP ports are available. + +```shell +export PYTHONPATH=../../:$PYTHONPATH +export CUDA_VISIBLE_DEVICES=0 +nohup python -u run_teacher1.py --use_cuda true --out_port 8080 > teacher1_online.log 2>&1& +export CUDA_VISIBLE_DEVICES=1,2 +nohup python -u run_teacher2.py --use_cuda true --out_port 8081 > teacher2_online.log 2>&1& +``` +Start the student model with the IP addresses that can reach the ports of the two teacher models, e.g., in the same node + +```shell +export PYTHONPATH=../../:$PYTHONPATH +python -u run_student.py \ + --in_address0 127.0.0.1:8080 \ + --in_address1 127.0.0.1:8081 \ +``` +**Note:** in online mode, the starting order of teachers and the sudent doesn't matter, and they will wait for each other to establish connection. + +3) **Hybrid of offline and online** + +One teacher works in offline mode and another one works in online mode. This time, start the offline teacher first. After the offline knowledge file gets well prepared, start the online teacher and the student at the same time. diff --git a/paddleslim/pantheon/__init__.py b/paddleslim/pantheon/__init__.py new file mode 100644 index 00000000..bcc99e78 --- /dev/null +++ b/paddleslim/pantheon/__init__.py @@ -0,0 +1,4 @@ +from .teacher import Teacher +from .student import Student + +__all__ = teacher.__all__ + student.__all__ diff --git a/paddleslim/pantheon/images/pantheon_arch.png b/paddleslim/pantheon/images/pantheon_arch.png new file mode 100644 index 0000000000000000000000000000000000000000..d88fc11f144e284fc64674a22cfd2554845b0176 GIT binary patch literal 98589 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 -|tK5NwQ25r$S;YnI@I~a? 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
Z)dVZC0Uh!DM8D{?KDI=DY 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 bcITj&)0ESot2@I1;CD|^{SHFmk^lbzvO*gn literal 0 HcmV?d00001 diff --git a/paddleslim/pantheon/student.py b/paddleslim/pantheon/student.py new file mode 100644 index 00000000..073e1865 --- /dev/null +++ b/paddleslim/pantheon/student.py @@ -0,0 +1,484 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed 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 six +import time +if six.PY2: + import cPickle as pickle +else: + import pickle + +import numpy as np +from collections import OrderedDict +from multiprocessing import Process, Manager +from multiprocessing.managers import BaseManager + +from threading import Thread + +from paddleslim.pantheon.utils import EndSignal, SyncSignal, StartSignal, public_authkey + +__all__ = ["Student"] + + +class Student(object): + """ + The class defined for the student model. Receive knowledge data from + teacher model and carry out knowledge merging. + + Args: + merge_strategy (dict|None): A dictionary whose keys are common + schemas shared by different teachers, and each corresponding + value specifies the merging strategy for different schemas + respectively, supporting 'sum' and 'mean' now. + """ + + def __init__(self, merge_strategy=None): + if merge_strategy: + for strategy in merge_strategy.values(): + if strategy not in ["sum", "mean"]: + raise ValueError( + "Merging strategy must be 'sum' or 'mean'!") + + self._merge_strategy = merge_strategy + self._common_schema = merge_strategy.keys() if merge_strategy else [] + + self._knowledge_desc = OrderedDict() + self._knowledge_queue = Manager().Queue(100) + self._teacher_knowledge_queues = [] + self._t2s_queues = [] + self._s2t_queues = [] + self._cmd_queues = [] + + self._num_teachers = 0 + + self._in_paths = [] + self._in_addresses = [] + + self._started = False + self._is_knowledge_desc_ready = False + self._is_knowledge_gen_locked = False + + def register_teacher(self, in_path=None, in_address=None): + """Register one teacher model and assign the order number to it as + its id, with the file path (offline mode) or IP address (online + mode) that the teacher model wrote knowledge data to. + + Args: + in_path (str|None): The input file path. Default None. + in_address (str|None): The input IP address, in the format + ":" (e.g. "127.0.0.1:8080"). Default None. + """ + if self._started: + raise ValueError( + "The student has been started and cannot register " + "teacher no longer!") + if in_path and in_address: + raise ValueError("Input path and input address should not " + "be given at the same time!") + if not in_path and not in_address: + raise ValueError("One of input path and input address should " + "be given when registering teacher!") + if in_address: + if in_address in self._in_addresses: + print("WARNING: the teacher with input address {} has been " + "registered, and ignored this time!".format(in_path)) + return + ip, port = in_address.strip().split(":") + BaseManager.register("get_knowledge_queue") + BaseManager.register("get_s2t_queue") + BaseManager.register("get_t2s_queue") + BaseManager.register("get_cmd_queue") + manager = BaseManager( + address=(ip, int(port)), authkey=public_authkey.encode()) + + # Wait for teacher model started to establish connection + print("Connecting to {}, with public key {} ...".format( + in_address, public_authkey)) + while True: + try: + manager.connect() + break + except: + time.sleep(1.0) + + knowledge_queue = manager.get_knowledge_queue() + self._t2s_queues.append(manager.get_t2s_queue()) + self._s2t_queues.append(manager.get_s2t_queue()) + self._cmd_queues.append(manager.get_cmd_queue()) + self._in_addresses.append(in_address) + self._in_paths.append(None) + print("Registered teacher {} with input address {}.".format( + self._num_teachers, in_address)) + else: + if in_path in self._in_paths: + print("WARNING: th teacher with input path {} has been " + "registered, and ignored this time!".format(in_path)) + return + + def read_offline(in_path, cmd_queue, out_queue): + end_recved = False + + def get_cmd(): + cmd, end_recved = None, False + try: + if not cmd_queue.empty(): + cmd = cmd_queue.get() + cmd_queue.task_done() + if isinstance(cmd, EndSignal): + end_recved = True + except IOError: + end_recved = True + return cmd, end_recved + + # wait for the sync in start + while not end_recved: + cmd, end_recved = get_cmd() + if isinstance(cmd, SyncSignal): + out_queue.put(SyncSignal()) + break + # for multiple-times offline serving + while not end_recved: + # wait for the sync in get_knowledge_desc() + while not end_recved: + cmd, end_recved = get_cmd() + if isinstance(cmd, SyncSignal): + out_queue.put(SyncSignal()) + break + + if end_recved: + break + with open(in_path, 'r') as fin: + # get knowledge desc + desc = pickle.load(fin) + out_queue.put(desc) + # wait for the data accessing signal + while not end_recved: + cmd, end_recved = get_cmd() + if isinstance(cmd, StartSignal): + break + # get knowledge data + while not end_recved: + try: + data = pickle.load(fin) + out_queue.put(data) + _, end_recved = get_cmd() + except EOFError: + break + if end_recved: + break + out_queue.put(EndSignal()) + out_queue.join() + + knowledge_queue = Manager().Queue(100) + cmd_queue = Manager().Queue(5) + p = Process( + target=read_offline, + args=(in_path, cmd_queue, knowledge_queue)) + p.daemon = True + p.start() + + self._t2s_queues.append(None) + self._s2t_queues.append(None) + self._cmd_queues.append(cmd_queue) + self._in_addresses.append(None) + self._in_paths.append(in_path) + print("Registered teacher {} with input path {}.".format( + self._num_teachers, in_path)) + + self._teacher_knowledge_queues.append(knowledge_queue) + self._num_teachers += 1 + + def _sync(self): + for i, queue in enumerate(self._cmd_queues): + if queue: + queue.put(SyncSignal()) + while True: + cmd = self._teacher_knowledge_queues[i].get() + self._teacher_knowledge_queues[i].task_done() + if isinstance(cmd, SyncSignal): + break + queue.join() + + def start(self): + """ + End teachers' registration and synchronize with all of them. + """ + + if self._started: + raise ValueError( + "The student cannot be started more than one time.") + self._sync() + self._started = True + + def _merge_knowledge(self, knowledge): + for k, tensors in knowledge.items(): + if len(tensors) == 0: + del knowledge[k] + elif len(tensors) == 1: + knowledge[k] = tensors[0] + else: + result = 0 + for tensor in tensors: + result += tensor + if self._merge_strategy[k] == "sum": + knowledge[k] = result + elif self._merge_strategy[k] == "mean": + knowledge[k] = result / len(tensors) + return knowledge + + def send(self, data, teacher_ids=None): + """ + Send data to teachers. + + Args: + data: A Python data object. + teacher_ids (list|None): A list of teacher ids to send data. If + set to None, send the data to all teachers. Default None. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if teacher_ids is None: + teacher_ids = range(self._num_teachers) + + for i in teacher_ids: + if self._s2t_queues[i]: + self._s2t_queues[i].put(data) + else: + print("Warning: didn't send data to teacher {} for it is in " + "offline mode.".format(i)) + + def recv(self, teacher_id): + """ + Receive data from one teacher. + + Args: + teacher_id (int): The id of teacher that receives data from. + + Return: + The received data object. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if self._t2s_queues[teacher_id]: + data = self._t2s_queues[teacher_id].get() + self._t2s_queues[teacher_id].task_done() + return data + else: + raise ValueError("Cannot receive data from teacher {} for it is " + "offline.".format(teacher_id)) + + def get_knowledge_desc(self): + """ + Get description for knowledge, including shape, data type and lod + level for each schema. + + Return: + dict: Knowledge description. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if self._is_knowledge_desc_ready == False: + self._sync() + # get knowledge description + knowledge_desc = OrderedDict() + for idx, queue in enumerate(self._teacher_knowledge_queues): + desc = queue.get() + queue.task_done() + if idx > 0 and (set(knowledge_desc.keys()) & set(desc.keys()) + != set(self._common_schema)): + raise ValueError( + "Teacher {} has the same schema with other existed " + "teachers not in the merge_strategy.".format(idx)) + knowledge_desc.update(desc) + + print("Knowledge merging strategy: {}".format( + self._merge_strategy)) + print("Knowledge description after merging:") + for schema, desc in knowledge_desc.items(): + print("{}: {}".format(schema, desc)) + + self._knowledge_desc = knowledge_desc + self._is_knowledge_desc_ready = True + return self._knowledge_desc + + def get_knowledge_qsize(self): + """ + Get the real-time size of knowledge queue. If this size is denoted as + **qsize**, it means that there are **qsize** batch knowledge data + already pushed into knowledge queue and waiting for the knowledge + generator to pop out. It's dynamic and limited up to 100, the capacity + of the knowledge queue. + + Return: + int: The real-time size of knowledge queue. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + return self._knowledge_queue.qsize() + + def get_knowledge_generator(self, batch_size, drop_last=False): + """ + Get the generator for knowledge data, return None if last generator + doesn't finish yet. + + Args: + batch_size (int): The batch size of returned knowledge data. + drop_last (bool): Whether to drop the last batch if its size is less + than batch size. + + Return: + func: The wrapper of knowledge data generator. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if batch_size <= 0: + raise ValueError("batch size must be positive!") + self._batch_size = batch_size + self._drop_last = drop_last + + # make sure only one generator is available at the same time + if self._is_knowledge_gen_locked: + print("WARNING: new knowledge generator is not available for the " + "last generator hasn't finished yielding all data yet! " + "Return None.") + return None + self._is_knowledge_gen_locked = True + + self.get_knowledge_desc() + + def split_batch(batch, num): + keys = batch.keys() + first, second = {}, {} + for key in keys: + first[key] = batch[key][0:num] + second[key] = batch[key][num:] + return first, second + + def concat_batches(batches): + keys = batches[0].keys() + ret_batch = {} + for key in keys: + ret_batch[key] = np.concatenate( + [batches[i][key] for i in range(len(batches))]) + return ret_batch + + def listen(queues, out_queue): + def data_receiver(queue, batch_size): + def wrapper(): + # The batch size of the teacher and student model may be + # not the same, make a new batch in the batch size of the + # student model. + batches, num_samples = [], 0 + while True: + batch_samples = queue.get() + queue.task_done() + if not isinstance(batch_samples, EndSignal): + cur_num_samples = list(batch_samples.values())[ + 0].shape[0] + if num_samples + cur_num_samples < batch_size: + batches.append(batch_samples) + num_samples += cur_num_samples + elif num_samples + cur_num_samples == batch_size: + batches.append(batch_samples) + yield concat_batches(batches) + batches, num_samples = [], 0 + else: + num_splited = batch_size - num_samples + first, second = split_batch(batch_samples, + num_splited) + batches.append(first) + yield concat_batches(batches) + num_left = cur_num_samples - num_splited + while num_left > batch_size: + first, second = split_batch(second, + batch_size) + yield first + num_left -= batch_size + batches, num_samples = [second], num_left + else: + if len(batches) > 0: + yield concat_batches(batches) + yield EndSignal() + break + + return wrapper + + data_receivers = [ + data_receiver(queue, self._batch_size)() for queue in queues + ] + + end_received = [0] * len(queues) + while True: + knowledge = OrderedDict( + [(k, []) for k, v in self._knowledge_desc.items()]) + for idx, receiver in enumerate(data_receivers): + if not end_received[idx]: + batch_samples = receiver.next( + ) if six.PY2 else receiver.__next__() + if not isinstance(batch_samples, EndSignal): + for k, v in batch_samples.items(): + knowledge[k].append(v) + else: + end_received[idx] = 1 + if sum(end_received) == len(queues): + break + knowledge = self._merge_knowledge(knowledge) + out_queue.put(knowledge) + out_queue.put(EndSignal()) + out_queue.join() + + # acquire data from teachers + for i, queue in enumerate(self._cmd_queues): + if queue: + queue.put(StartSignal()) + queue.join() + + self._listen_thread = Thread( + target=listen, + args=(self._teacher_knowledge_queues, self._knowledge_queue)) + self._listen_thread.dameon = True + self._listen_thread.start() + + def wrapper(): + samples = [] + + while True: + knowledge = self._knowledge_queue.get() + self._knowledge_queue.task_done() + if not isinstance(knowledge, EndSignal): + batch_size = list(knowledge.values())[0].shape[0] + if (batch_size < self._batch_size) and drop_last: + continue + yield knowledge + else: + break + # After all knowledge data yielded, make current knowledge desc invalid. + self._is_knowledge_desc_ready = False + self._is_knowledge_gen_locked = False + + return wrapper + + def __del__(self): + for i, path in enumerate(self._in_paths): + if path: + try: + self._cmd_queues[i].put(EndSignal()) + self._cmd_queues[i].join() + except: + pass diff --git a/paddleslim/pantheon/teacher.py b/paddleslim/pantheon/teacher.py new file mode 100644 index 00000000..6cd09efb --- /dev/null +++ b/paddleslim/pantheon/teacher.py @@ -0,0 +1,501 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed 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 os +import time +import six +if six.PY2: + import cPickle as pickle + import Queue +else: + import pickle + import queue as Queue + +from collections import OrderedDict, Iterable +import numpy as np +import copy +import multiprocessing +from multiprocessing.managers import BaseManager +from threading import Thread + +import paddle.fluid as fluid + +from paddleslim.pantheon.utils import convert_dtype, EndSignal, SyncSignal, StartSignal, public_authkey + +__all__ = ["Teacher"] + +knowledge_queue = Queue.Queue(100) +t2s_queue = Queue.Queue(100) +s2t_queue = Queue.Queue(100) +cmd_queue = Queue.Queue(5) + + +class MixedDataReader(object): + """ + The wrapper for iterable data loader, to solve the drop problem of last + batches when their number is less than the number of devices in prediction. + It implements two data generators, one for the prediction on all devices, + and another one for the prediction of remained data one single device, and + they two should be called in order. + + Args: + data_loader (fluid.io.DataLoader): The data loader. + base_number (int): The base number that the number of yielded data + batches for multiple devices should be its + multiple times. + """ + + def __init__(self, data_loader, base_number): + self._data_loader = data_loader + self._base_number = base_number + self._tail_data = [] + + def multi_dev_generator(self): + for data in self._data_loader(): + if len(self._tail_data) < self._base_number: + self._tail_data += data + if len(self._tail_data) == self._base_number: + yield self._tail_data + self._tail_data = [] + + def tail_generator(self): + for data in self._tail_data: + yield data + self._tail_data = [] + + +class Teacher(object): + """ + The class defined for the teacher model. Generate knowledge data and + transfer them to the student model. + + Args: + out_path (str|None): The path to dump knowledge for offline mode. + out_port (int|None): The IP port number to send out knowledge for + online mode, should be unique when launching multiple teachers in + the same node. + """ + + def __init__(self, out_path=None, out_port=None): + if out_path and out_port: + raise ValueError("Out path and out port should not be set at " + "the same time!") + + self._out_path = out_path + self._out_port = out_port + # knowledge description + self._knowledge_desc = {} + + self._sync_required = False + self._data_required = False + self._started = False + + def _start_manager(self): + def get_knowledge_queue(): + global knowledge_queue + return knowledge_queue + + def get_s2t_queue(): + global s2t_queue + return s2t_queue + + def get_t2s_queue(): + global t2s_queue + return t2s_queue + + def get_cmd_queue(): + global cmd_queue + return cmd_queue + + BaseManager.register( + "get_knowledge_queue", callable=get_knowledge_queue) + BaseManager.register("get_s2t_queue", callable=get_s2t_queue) + BaseManager.register("get_t2s_queue", callable=get_t2s_queue) + BaseManager.register("get_cmd_queue", callable=get_cmd_queue) + manager = BaseManager( + address=("", self._out_port), authkey=public_authkey.encode()) + manager.start() + print("listen on address: {}".format(manager._address)) + print("public authkey: {}".format(public_authkey)) + return manager + + def start(self): + """ + Start teacher service, sychronize with student and launch the thread + to monitor commands from student. + """ + if self._started: + raise ValueError( + "The teacher cannot be started more than one time.") + self._started = True + self._manager = self._start_manager() if self._out_port else None + if self._manager: + self._knowledge_queue = self._manager.get_knowledge_queue() + self._s2t_queue = self._manager.get_s2t_queue() + self._t2s_queue = self._manager.get_t2s_queue() + self._cmd_queue = self._manager.get_cmd_queue() + else: + self._knowledge_queue = None + self._s2t_queue = None + self._t2s_queue = None + self._cmd_queue = None + + self._out_file = open(self._out_path, "w") if self._out_path else None + if self._out_file: + return + + def wrapper(): + while True: + if not self._cmd_queue.empty(): + cmd = self._cmd_queue.get() + self._cmd_queue.task_done() + if isinstance(cmd, SyncSignal): + self._sync_required = True + elif isinstance(cmd, StartSignal): + self._data_required = True + else: + time.sleep(1.0) + + t = Thread(target=wrapper) + t.daemon = True + t.start() + + while True: + if self._sync_required: + self._knowledge_queue.put(SyncSignal()) + self._knowledge_queue.join() + self._sync_required = False + break + + def send(self, data): + """ + Send one data object to student. + + Args: + data (Python data): The data to be sent, can be any type of Python data object. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if not self._t2s_queue: + raise ValueError("Cannot send data to stuent for this teacher " + "is offline!") + self._t2s_queue.put(data) + + def recv(self): + """ + Recieve one data object from student. + + Return: + The received data, can be any type of Python data object. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if not self._s2t_queue: + raise ValueError( + "Cannot receive data from stuent for this teacher " + "is in offline mode!") + data = self._s2t_queue.get() + self._s2t_queue.task_done() + return data + + def dump(self, knowledge): + """ + Dump one batch knowledge data into output file, only used in the + offline mode. + + Args: + knowledge (dict): The knowledge data to be dumped. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if not self._out_file: + raise ValueError("Cannot dump knowledge data in online mode!") + + if not isinstance(knowledge, dict) and not isinstance(knowledge, + OrderedDict): + raise ValueError( + "The knowledge data should be a dict or OrderedDict!") + + knowledge_desc = {} + for name, value in knowledge.items(): + knowledge_desc[name] = { + "shape": [-1] + list(value.shape[1:]), + "dtype": str(value.dtype), + "lod_level": 0 + } + if not self._knowledge_desc: + self._knowledge_desc = knowledge_desc + self._out_file.write(pickle.dumps(self._knowledge_desc)) + else: + if self._knowledge_desc != knowledge_desc: + raise ValueError( + "Current knowledge desc {} is not the same as " + "historic desc {}!".format(knowledge_desc, + self._knowledge_desc)) + + self._out_file.write(pickle.dumps(knowledge)) + + def start_knowledge_service(self, + feed_list, + schema, + program, + reader_config, + exe, + buf_size=10, + times=1): + """ + Start the knowledge service to generate and transfer knowledge data. + In GPU mode, the devices to execute knowledge prediction will be + determined by environment variable **FLAGS_selected_gpus**, or by + **CUDA_VISIBLE_DEVICES** if it is not set, and by **CPU_NUM** (default + 1) in CPU mode. Only supported in static graph. + + Args: + feed_list (list): A list of feed Variables or their names for the + input program. + schema (dict): A dictionary to specify names and fetched + Variables of knowledge. + program (fluid.Program): Inference program for the teacher model. + reader_config (dict): The config for data reader. Support all the + three types of generators used by `fluid.io.PyReader` and + `fluid.io.DataLoader`, and their configs contain the key-value + pair of the generator type and a generator object, plus + other necessary argument pairs. See the following: + + 1) sample generator: + reader_config={"sample_generator": #some_sample_generator, + "batch_size": #batch_size, "drop_last": #drop_last}, + 'drop_last' set to True by default, + 2) sample list generator: + reader_config={"sample_list_generator": + #some_sample_list_generator}, + 3) batch generator: + reader_config={"batch_generator": #some_batch_genrator}. + + The trial to parse config will be in the order of 1) -> 3), and + any other unrelated keys in these configs will be ignored. + exe (fluid.Executor): The executor to run the input program. + buf_size (int): The size of buffers for data reader and knowledge + writer on each device. + times (int): The maximum repeated serving times. Default 1. Whenever + the public method 'get_knowledge_generator()' in Student + object called once, the serving times will be added one, + until reaching the maximum and ending the service. + """ + if not self._started: + raise ValueError("The method start() should be called first!") + + if not isinstance(program, fluid.Program): + raise ValueError( + "Input argument 'program' should be a fluid Program!") + self._program = program._inference_optimize(prune_read_op=True) + + if not isinstance(feed_list, list): + raise ValueError("Input argument 'feed_list' should be a list!") + else: + self._feed_list = [] + for feed in feed_list: + if isinstance(feed, fluid.framework.Variable): + self._feed_list.append(feed) + elif isinstance(feed, str) or isinstance(feed, unicode): + self._feed_list.append(self._program.global_block().var( + feed)) + else: + raise ValueError( + "Input 'feed_list' should consist of feed " + "Variables or their names!") + + if not isinstance(schema, dict) and not isinstance(schema, + OrderedDict): + raise ValueError( + "Input argument 'schema' should be a dict or OrderedDict!") + self._schema = schema + + if not isinstance(reader_config, dict): + raise ValueError("The reader config must be a dictionary!") + + if not isinstance(exe, fluid.Executor): + raise ValueError("Input argument should be a fluid Executor!") + self._exe = exe + + if not buf_size > 0: + raise ValueError("The buffer size should be positive!") + self._buf_size = buf_size + + if not times > 0: + raise ValueError("Repeated serving times should be positive!") + self._times = times + + desc = {} + for name, var in schema.items(): + if not isinstance(var, fluid.framework.Variable): + raise ValueError( + "The member of schema must be fluid Variable.") + desc[name] = { + "shape": var.shape, + "dtype": convert_dtype(var.dtype), + "lod_level": var.lod_level + } + if not self._knowledge_desc: + self._knowledge_desc = desc + else: + if self._out_file and not self._knowledge_desc == desc: + raise ValueError("The knowledge description should be kept " + "consistent in offline mode!") + + if isinstance(self._exe.place, fluid.CUDAPlace): + places = fluid.cuda_places() + else: + places = fluid.cpu_places() + dev_count = len(places) + + data_loader = fluid.io.DataLoader.from_generator( + feed_list=self._feed_list, + capacity=self._buf_size * dev_count, + use_double_buffer=(dev_count == 1), + iterable=True) + + places = [fluid.CPUPlace()] if dev_count > 1 else [self._exe.place] + if "sample_generator" in reader_config: + if "batch_size" not in reader_config: + raise ValueError("batch size must be specified when using " + "sample generator!") + sample_generator = reader_config["sample_generator"] + batch_size = reader_config["batch_size"] + drop_last = reader_config[ + "drop_last"] if "drop_last" in reader_config else True + + data_loader.set_sample_generator( + reader=sample_generator, + batch_size=batch_size, + drop_last=drop_last, + places=places) + elif "sample_list_generator" in reader_config: + sample_list_generator = reader_config["sample_list_generator"] + data_loader.set_sample_list_generator( + reader=sample_list_generator, places=places) + elif "batch_generator" in reader_config: + batch_generator = reader_config["batch_generator"] + data_loader.set_batch_generator( + reader=batch_generator, places=places) + else: + raise ValueError( + "The reader config doesn't contain any valid " + "generator type, which should be one of 'sample_generator', " + "'sample_list_generator', and 'batch_generator'.") + + def writer(buf_queue, schema_keys): + samples_sent, batches_sent = 0, 0 + while True: + outputs = buf_queue.get() + buf_queue.task_done() + if not isinstance(outputs, EndSignal): + batch_samples = dict(zip(schema_keys, outputs)) + if self._knowledge_queue: + self._knowledge_queue.put(batch_samples) + if self._out_file: + self._out_file.write(pickle.dumps(batch_samples)) + else: + if self._knowledge_queue: + self._knowledge_queue.put(EndSignal()) + + # Asynchronous output + out_buf_queue = Queue.Queue(self._buf_size) + schema_keys, schema_vars = zip(*self._schema.items()) + out_thread = Thread(target=writer, args=(out_buf_queue, schema_keys)) + out_thread.daemon = True + out_thread.start() + + compiled_program = fluid.compiler.CompiledProgram( + self._program).with_data_parallel() + + print("Knowledge description {}".format(self._knowledge_desc)) + print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + + " Teacher begins to serve ...") + # For offline dump, write the knowledge description to the head of file + if self._out_file: + self._out_file.write(pickle.dumps(self._knowledge_desc)) + print("output path: %s" % self._out_path) + + data_reader = MixedDataReader(data_loader, dev_count) + # For online mode, send knowledge description every time + for repeated in range(self._times): + if self._knowledge_queue: + # wait for the accessing of knowledge desc and data + while True: + if self._sync_required: + self._knowledge_queue.put(SyncSignal()) + self._knowledge_queue.put(self._knowledge_desc) + self._sync_required = False + if self._data_required: + self._data_required = False + break + self._knowledge_queue.join() + + print("No.{} time serving ... ".format(repeated)) + num_batches_sent = 0 + for dev_batches in data_reader.multi_dev_generator(): + if self._sync_required: + break + outputs = self._exe.run(compiled_program, + feed=dev_batches, + fetch_list=schema_vars) + out_buf_queue.put(outputs) + num_batches_sent += dev_count + if num_batches_sent % (100 * dev_count) == 0: + log = "Processed {} batch samples.".format( + num_batches_sent) + if self._knowledge_queue: + log += " Knowledge queue size {}.".format( + self._knowledge_queue.qsize()) + print(log) + + outputs = [] + for index, batch in enumerate(data_reader.tail_generator()): + if self._sync_required: + break + output = self._exe.run(self._program, + feed=batch, + fetch_list=schema_vars) + if outputs: + outputs = [ + np.concatenate( + (outs, out), axis=0) + for (outs, out) in zip(outputs, output) + ] + else: + outputs = copy.deepcopy(output) + if outputs: + out_buf_queue.put(outputs) + num_batches_sent += (index + 1) + + print("Processed {} batch samples in total.".format( + num_batches_sent)) + + out_buf_queue.put(EndSignal()) + out_buf_queue.join() + + if self._knowledge_queue: + self._knowledge_queue.join() + print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) + + " Teacher ends serving.") + + def __del__(self): + if self._manager: + self._manager.shutdown() + if self._out_file: + self._out_file.close() diff --git a/paddleslim/pantheon/utils.py b/paddleslim/pantheon/utils.py new file mode 100644 index 00000000..b4c8001e --- /dev/null +++ b/paddleslim/pantheon/utils.py @@ -0,0 +1,61 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed 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 collections + +public_authkey = u"aBcXyZ123" + + +class StartSignal(): + pass + + +class EndSignal(): + pass + + +class SyncSignal(): + pass + + +def convert_dtype(dtype): + import paddle.fluid as fluid + if isinstance(dtype, fluid.core.VarDesc.VarType): + if dtype == fluid.core.VarDesc.VarType.BOOL: + return 'bool' + elif dtype == fluid.core.VarDesc.VarType.FP16: + return 'float16' + elif dtype == fluid.core.VarDesc.VarType.FP32: + return 'float32' + elif dtype == fluid.core.VarDesc.VarType.FP64: + return 'float64' + elif dtype == fluid.core.VarDesc.VarType.INT8: + return 'int8' + elif dtype == fluid.core.VarDesc.VarType.INT16: + return 'int16' + elif dtype == fluid.core.VarDesc.VarType.INT32: + return 'int32' + elif dtype == fluid.core.VarDesc.VarType.INT64: + return 'int64' + elif dtype == fluid.core.VarDesc.VarType.UINT8: + return 'uint8' + + +def check_ip(address): + import IPy + try: + IPy.IP(address) + return True + except Exception as e: + return False -- GitLab