Giter Club home page Giter Club logo

mlora's Introduction

mLoRA

An Efficient "Factory" to Build Multiple LoRA Adapters

mLoRA (a.k.a Multi-LoRA Fine-Tune) is an open-source framework designed for efficient fine-tuning of multiple Large Language Models (LLMs) using LoRA and its variants. Key features of mLoRA include:

  • Concurrent fine-tuning of multiple LoRA adapters.

  • Shared base model among multiple LoRA adapters.

  • Efficient pipeline parallelism algorithm.

  • Support for multiple LoRA variant algorithms and various base models.

  • Support for multiple reinforcement learning preference alignment algorithms.

The end-to-end architecture of the mLoRA is shown in the figure:

Quickstart

Firstly, you should clone this repository and install dependencies (or use our image):

# Clone Repository
git clone https://github.com/TUDB-Labs/mLoRA
cd mLoRA
# Install requirements need the Python >= 3.12
pip install .

The mlora_train.py code is a starting point for batch fine-tuning LoRA adapters.

python mlora_train.py \
  --base_model TinyLlama/TinyLlama-1.1B-Chat-v0.4 \
  --config demo/lora/lora_case_1.yaml

You can check the adapters' configuration in demo folder, there are some configuration regarding the use of different LoRA variants and reinforcement learning preference alignment algorithms.

For further detailed usage information, please use --help option:

python mlora_train.py --help

Deployment using pipeline parallelism

Similar to Quickstart, the command to start in a two-node environment is as follows:

NOTE1: Use environment variables MASTER_ADDR/MASTER_PORT to set the master node.

NOTE2: Set balance, indicating the number of decoder layers allocated to each rank.

# in the first node
export MASTER_ADDR=master.svc.cluster.local
export MASTER_PORT=12355
python mlora_pp_train.py \
  --base_model TinyLlama/TinyLlama-1.1B-Chat-v0.4 \
  --config demo/lora/lora_case_1.yaml \
  --pipeline \
  --device "cuda:0" \
  --rank 0 \
  --balance 12 13 \
  --recompute False \
  --precision fp32

# in the second node
export MASTER_ADDR=master.svc.cluster.local
export MASTER_PORT=12355
python mlora_pp_train.py \
  --base_model TinyLlama/TinyLlama-1.1B-Chat-v0.4 \
  --config demo/lora/lora_case_1.yaml \
  --pipeline \
  --device "cuda:1" \
  --rank 1 \
  --balance 12 13 \
  --recompute False \
  --precision fp32

Quickstart with Docker

mLoRA offers an official Docker image for quick start and development, The image is available on Dockerhub Packages registry.

First, you should pull the latest image (the image also use for development):

docker pull yezhengmaolove/mlora:latest

Deploy and enter a container to run mLoRA:

docker run -itd --runtime nvidia --gpus all \
    -v ~/your_dataset_dir:/dataset \
    -v ~/your_model_dir:/model \
    -p <host_port>:22 \
    --name mlora \
    yezhengmaolove/mlora:latest
# when the container started, use the ssh to login
# the default password is mlora@123
ssh root@localhost -p <host_port>
# pull the latest code and run the mlora
cd /mLoRA
git pull
python mlora_train.py \
  --base_model TinyLlama/TinyLlama-1.1B-Chat-v0.4 \
  --config demo/lora/lora_case_1.yaml

Deploy as service with Docker

We can deploy mLoAR as a service to continuously receive user requests and perform fine-tuning task.

First, you should pull the latest image (use same image for deploy):

docker pull yezhengmaolove/mlora:latest

Deploy our mLoRA server:

docker run -itd --runtime nvidia --gpus all \
    -v ~/your_dataset_cache_dir:/cache \
    -v ~/your_model_dir:/model \
    -p <host_port>:8000 \
    --name mlora_server \
    -e "BASE_MODEL=TinyLlama/TinyLlama-1.1B-Chat-v0.4" \
    -e "STORAGE_DIR=/cache" \
    yezhengmaolove/mlora:latest /bin/bash /opt/deploy.sh

Once the service is deployed, install and use mlora_cli.py to interact with the server.

# install the client tools
pip install mlora-cli
# use the mlora cli tool to connect to mlora server
mlora_cli
(mLoRA) set port <host_port>
(mLoRA) set host http://<host_ip>
# and enjoy it!!
Step-by-step

Step1. Download the mlora image and install the mlora_cli

docker pull yezhengmaolove/mlora:latest
pip install mlora-cli

asciicast

Step2. Start the mlora server with Docker

# first, we create a cache dir in host for cache some file
mkdir ~/cache
# second, we manually download the model weights from Hugging Face.
mkdir ~/model && cd ~/model
git clone https://huggingface.co/TinyLlama/TinyLlama-1.1B-Chat-v1.0
# we map port 8000 used by the mlora server to port 1288 on the host machine.
# the BASE_MODEL environment variable indicates the path of the base model used by mlora.
# the STORAGE_DIR environment variable indicates the path where datasets and lora adapters are stored.
# we use the script /opt/deploy.sh in container to start the server.
docker run -itd --runtime nvidia --gpus all \
    -v ~/cache:/cache \
    -v ~/model:/model \
    -p 1288:8000 \
    --name mlora_server \
    -e "BASE_MODEL=/model/TinyLlama-1.1B-Chat-v1.0" \
    -e "STORAGE_DIR=/cache" \
    yezhengmaolove/mlora:latest /bin/bash /opt/deploy.sh

asciicast

Step3. use mlora_cli tool link to mlora server

we use mlora_cli link to the server http://127.0.0.1:1288 (must use the http protocal)

(mLoRA) set port 1288
(mLoRA) set host http://127.0.0.1

asciicast

Step4. upload some data file for train.

we use the Stanford Alpaca dataset as a demo, the data just like below:

[{"instruction": "", "input": "", "output": }, {...}]
(mLoRA) file upload
? file type: train data
? name: alpaca
? file path: /home/yezhengmao/alpaca-lora/alpaca_data.json

asciicast

Step5. upload some template to provide a structured format for generating prompts

the template in a yaml file, and write by templating language Jinja2, see the demo/prompt.yaml file

the data file you upload can be considered as array data, with the elements in the array being of dictionary type. we consider each element as a data point in the template.

(mLoRA) file upload
? file type: prompt template
? name: simple_prompt
? file path: /home/yezhengmao/mLoRA/demo/prompt.yaml

asciicast

Step6. create a dataset

we create a dataset, the dataset consists of data, a template, and the corresponding prompter. we can use dataset showcase command to check the if the prompts are generated correctly.

(mLoRA) dataset create
? name: alpaca_dataset
? train data file: alpaca
? prompt template file: simple_prompt
? prompter: instruction
? data preprocessing: default
(mLoRA) dataset showcase
? dataset name: alpaca_dataset

asciicast

Step7. create a adapter

now we can use adapter create command to create a adapter for train.

asciicast

Step8. !!!! submit task to train !!!!

Finally, we can submit the task to train our adapter using the defined dataset. NOTE: you can continuously submit or terminal training tasks. use the adapter ls or task ls to check the tasks' status

asciicast

Why you should use mLoRA

Using mLoRA can save significant computational and memory resources when training multiple adapters simultaneously.

High performance on consumer hardware

We fine-tuned multiple LoRA adapters using four A6000 graphics cards with fp32 precision and without using checkpointing and any quantization techniques:

Model mLoRA (tokens/s) PEFT-LoRA with FSDP (tokens/s) PEFT-LoRA with TP (tokens/s)
llama-2-7b (32fp) 2364 1750 1500
llama-2-13b (32fp) 1280 OOM 875

Supported model

Model
LLaMA

Supported LoRA variants

Variant
QLoRA,NIPS,2023
LoRA+,ICML,2024
VeRA,ICLR,2024
DoRA,ICML,2024

Supported preference alignment algorithms

Variant
DPO,NeurIPS,2024
CPO,ICML,2024
CIT,arXiv,2024

Document

Contributing

We welcome contributions to improve this repository! Please review the contribution guidelines before submitting pull requests or issues.

Fork the repository. Create a new branch for your feature or fix. Submit a pull request with a detailed explanation of your changes.

You can use the pre-commit to check your code.

# Install requirements
pip install .[ci_test]
ln -s ../../.github/workflows/pre-commit .git/hooks/pre-commit

Or just call the script to check your code

.github/workflows/pre-commit

Citation

Please cite the repo if you use the code in this repo.

@misc{m-LoRA,
  author = {Zhengmao, Ye\textsuperscript{*} and Dengchun, Li\textsuperscript{*} and Jingqi, Tian and Tingfeng, Lan and Yanbo, Liang and Yexi, Jiang and Jie, Zuo and Hui, Lu and Lei, Duan and Mingjie, Tang},
  title = {m-LoRA: Efficient LLM Model Fine-tune and Inference via Multi-Lora Optimization},
  year = {2023},
  publisher = {GitHub},
  howpublished = {\url{https://github.com/TUDB-Labs/mLoRA}},
  note={\textsuperscript{*}: these authors contributed equally to this work.}
}

Copyright

Copyright © 2024 All Rights Reserved.

This project is licensed under the Apache 2.0 License.

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.

mlora's People

Contributors

antlera avatar cainiaogoroad avatar lianxingao avatar merlintang avatar mikecovlee avatar qsimu avatar trilarflagz avatar vinkle-hzt avatar waitfor-night avatar yezhengmao1 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

mlora's Issues

Will Embeding change?

I have been studying LoRA recently and I noticed that during pre-training, the word vectors change as the training progresses. However, what about when using LoRA for fine-tuning? Do the word vectors still change, or is it only the attention weights.

Cannot load vicuna-7b-delta-v0

  1. Use aspen.load_llama_tf_weight to load vicuna-7b-delta-v0 model use more than 30GB memory then caused OOM.

  2. Use utils.convert_hf_to_pth to transfer vicuna-7b-delta-v0 to .pth model, then use aspen.load_llama_7b_weight to load .pth model, an error is reported:

Not use layer model.embed_tokens.weight.
Traceback (most recent call last):
  File "/data/glx/code/multi_lora/legacy.py", line 43, in <module>
    aspen.load_llama_7b_weight(llama_model, config["base_model"], config["device"])
  File "/data/glx/code/multi_lora/aspen/modelloader.py", line 21, in load_llama_7b_weight
    layer_id = int(layer_name[:layer_name.find(".")])
ValueError: invalid literal for int() with base 10: 'ayers'

How to test the performance.

Use patch files below to get baseline performance(alpaca-Lora):
transformers/trainer.py

148a149,155
> import logging as flog
> import os
> import time
> flog.basicConfig(filename="logs.log",
>                  filemode='a',
>                  format='%(message)s',
>                  level=flog.DEBUG)
1871a1879,1880
>                     torch.cuda.reset_peak_memory_stats()
>                     opti_start_time = time.time()
1894a1904,1909
>                     opti_end_time = time.time()
>                     device_str = inputs["input_ids"].device
>                     alloc_mem = torch.cuda.max_memory_allocated(device_str)
>                     gpu_utilization = torch.cuda.utilization(int(os.environ["CUDA_VISIBLE_DEVICES"]))
>                     flog.info(f"optim: {(opti_end_time - opti_start_time):.10f} {alloc_mem} {gpu_utilization}")
>                     flog.info(f"train: {tr_loss_step}")
2658a2674,2675
>         torch.cuda.reset_peak_memory_stats()
>         back_start_time = time.time()
2665a2683,2687
>         back_end_time = time.time()
>         device_str = inputs["labels"].device
>         alloc_mem = torch.cuda.max_memory_allocated(device_str)
>         gpu_utilization = torch.cuda.utilization(int(os.environ["CUDA_VISIBLE_DEVICES"]))
>         flog.info(f"backward: {(back_end_time - back_start_time):.10f} {alloc_mem} {gpu_utilization}")

transformers/models/llama/modeling_llama.py

38a39,46
> import logging as flog
> import os
> import time
> flog.basicConfig(filename="logs.log",
>                  filemode='a',
>                  format='%(message)s',
>                  level=flog.DEBUG)
>
805a814,816
>         flog.info(f"data size: {input_ids.shape[0]} {input_ids.shape[1]}")
>         torch.cuda.reset_peak_memory_stats()
>         forward_start_time = time.time()
816a828,832
>         forward_end_time = time.time()
>         device_str = input_ids.device
>         alloc_mem = torch.cuda.max_memory_allocated(device_str)
>         gpu_uilization = torch.cuda.utilization(int(os.environ["CUDA_VISIBLE_DEVICES"]))
>         flog.info(f"forward: {(forward_end_time - forward_start_time):.10f} {alloc_mem} {gpu_uilization}")
837a854,856
>
>             torch.cuda.reset_peak_memory_stats()
>             loss_start_time = time.time()
838a858,862
>             loss_end_time = time.time()
>             device_str = input_ids.device
>             alloc_mem = torch.cuda.max_memory_allocated(device_str)
>             gpu_uilization = torch.cuda.utilization(int(os.environ["CUDA_VISIBLE_DEVICES"]))
>             flog.info(f"loss: {(loss_end_time - loss_start_time):.10f} {alloc_mem} {gpu_uilization}")

peft/tuners/lora.py

46a47,53
> import logging as flog
> import os
> import time
> flog.basicConfig(filename="logs.log",
>                  filemode='a',
>                  format='%(message)s',
>                  level=flog.DEBUG)
1148a1156,1157
>             torch.cuda.reset_peak_memory_stats()
>             base_start_time = time.time()
1149a1159,1163
>             base_end_time = time.time()
>             device_str = x.device
>             alloc_mem = torch.cuda.max_memory_allocated(device_str)
>             gpu_utilization = torch.cuda.utilization(int(os.environ["CUDA_VISIBLE_DEVICES"]))
>             flog.info(f"base: {(base_end_time-base_start_time):.10f} {alloc_mem} {gpu_utilization}")
1153a1168,1169
>                 torch.cuda.reset_peak_memory_stats()
>                 lora_start_time = time.time()
1172a1189,1193
>                 lora_end_time = time.time()
>                 device_str = x.device
>                 alloc_mem = torch.cuda.max_memory_allocated(device_str)
>                 gpu_utilization = torch.cuda.utilization(int(os.environ["CUDA_VISIBLE_DEVICES"]))
>                 flog.info(f"lora: {(lora_end_time-lora_start_time):.10f} {alloc_mem} {gpu_utilization}")

Performance Test

Self comparison test about alpaca_data_en_52k dataset on vicuna-7b-v1.1 (GPU: A100), group_by_length and no checkpoint.

Method1: Using the same configuration file and data, fine-tune two datasets simultaneously.
Method2: Using the same configuration file and data, fine-tune only one dataset.

method 1 config file:

{
    "cutoff_len": 256,
    "group_by_length": true,
    "expand_right": true,
    "pad_token_id": -1,
    "save_step": 20000,
    "lora": [
        {
            "name": "lora_0",
            "output": "lora_0",
            "optim": "adamw",
            "lr": 1e-4,
            "batch_size": 16,
            "num_epochs": 1,
            "r": 8,
            "alpha": 16,
            "dropout": 0.05,
            "target_modules": {
                "q_proj": true,
                "k_proj": true,
                "v_proj": true,
                "o_proj": true,
                "w1_proj": false,
                "w2_proj": false,
                "w3_proj": false
            },
            "data": "/data/glx/LLaMA-Efficient-Tuning/data/alpaca_data_en_52k.json",
            "prompt": "template/template_demo.json"
        },
        {
            "name": "lora_1",
            "output": "lora_1",
            "optim": "adamw",
            "lr": 1e-4,
            "batch_size": 16,
            "num_epochs": 1,
            "r": 8,
            "alpha": 16,
            "dropout": 0.05,
            "target_modules": {
                "q_proj": true,
                "k_proj": true,
                "v_proj": true,
                "o_proj": true,
                "w1_proj": false,
                "w2_proj": false,
                "w3_proj": false
            },
            "data": "/data/glx/LLaMA-Efficient-Tuning/data/alpaca_data_en_52k.json",
            "prompt": "template/template_demo.json"
        }
    ]
}

method2 config file:

{
    "cutoff_len": 256,
    "group_by_length": true,
    "expand_right": true,
    "pad_token_id": -1,
    "save_step": 20000,
    "lora": [
        {
            "name": "lora_only1",
            "output": "lora_only1",
            "optim": "adamw",
            "lr": 1e-4,
            "batch_size": 16,
            "num_epochs": 1,
            "r": 8,
            "alpha": 16,
            "dropout": 0.05,
            "target_modules": {
                "q_proj": true,
                "k_proj": true,
                "v_proj": true,
                "o_proj": true,
                "w1_proj": false,
                "w2_proj": false,
                "w3_proj": false
            },
            "data": "/data/glx/LLaMA-Efficient-Tuning/data/alpaca_data_en_52k.json",
            "prompt": "template/template_demo.json"
        }
    ]
}

Method1: time cost: 7h55min, gpu memory cost: 21.74GB
Method2: time cost: 4h17min, gpu memory cost: 15.86GB

Log style not consistent

Consider replace current log mechanism with a unified log framework.

[2023-12-11 21:36:41] m-LoRA: NVIDIA CUDA initialized successfully.
[2023-12-11 21:36:41] m-LoRA: Total 1 GPU(s) detected.
[2023-12-11 21:36:41] m-LoRA: Loading model with quantization, bits = 8

example goes out of memory

Dear, Author,

Thanks for this great project,
I hit a problem when I tried to run the example code mlora.py with float16
I use A100 with 40GB memory but it still goes out of memory.

Do you have any clue about this error?

Thanks!

[WIP] Aspen test report

We randomly generated 4 datasets, 1/2 train data set randomly chosen from alpaca-lora, and 3 and 4 from spider. below are the datasets' token lens distribution and total size.
test gpu: a6000
data_set_1: 34000
data_set_2: 17000
data_set_3: 5556
data_set_4: 2700
We will train 8 lora model:
data_set_1 with lr = 3e-4 and lr = 1e-4
data_set_2 with lr = 3e-4 and lr = 1e-4
data_set_3 with lr = 3e-4 and lr = 1e-4
data_set_4 with lr = 3e-4 and lr = 1e-4

train 2 lora model parallel in 2 gpu
= one lora in gpu0, and another lora in gpu1
= train 2 lora model serial in one gpu, and ignore the model load time.
双卡并行时延VS单卡ASPEN并行时延

train 2 lora model parallel in 1 gpu
单卡并行时延VS单卡ASPEN并行时延

compare the peak memory
ALPACA-LORAVS单卡ASPEN并行显存
compare a6000 and 4090
4090VSA6000

ImportError: cannot import name 'override' from 'typing'

i excute this commad:python mlora.py --base_model TinyLlama/TinyLlama-1.1B-Chat-v0.4 --config ./demo/dummy.yaml ,but it tell me:ImportError: cannot import name 'override' from 'typing' .

all output of this error are as follows:
image

checkpoint offload policy

Now we use the checkpoint to save GPU memory, The checkpoint will cache each transformer layer's input, and forward without grad produce, and then backward will use the input data cached to recompute and produce each transformer layer's grad.

but I think if the tensor size is big(train multi Lora model has a big total batch size, so the tensor is enough big.) the time taken by recompute will be less than transfer time(GPU -> CPU and CPU -> GPU). Maybe this method will increase latency but will increase throughput.

I have found some APIs to implement it:

  • save_on_cpu will save the checkpoint's input to the CPU, and when needed backward, it will load the input data from CPU to GPU. This API just saves the checkpoint's input memory.
  • saved_tensors_hooks Use this context-manager can define how intermediary results of an operation should be packed before saving, and unpacked on retrieval. So it can offload tensor when forward and backward. The torch==2.0.1 uses this API to implement checkpoint, but does not support the user passing the argument to change it, if we want to implement our policy, can hack this function.
  • pytorch-v2.1.0-rc2 in the nightly version, I found the checkpoint API allow the user to create new context manager, we can implement our context manager to implement the offload policy.

The test report compare to alpaca-lora.

I tested three different datasets with different amounts in alpaca-lora and multi-lora-fine-tune.
Each dataset(the input data's sequence and size are also the same) trains two different lora models with two different optimizers, each optimizer has the same training hyperparams.
So the alpaca-lora needs to be trained twice to produce two different lora model, but multi-lora-fine-tune just need once to produce one lora model.
The experimental statistics on end-to-end train latency (without model and dataset load and save latency).

  • dataset1 use batchsize 7, 457 data from alpaca-lora, and max seq len is 1304
  • dataset2 use batchsize 16, 452 data from alpaca-lora, and max seq len is 512
  • dataset3 use batchsize 16, 5000 data from sql-create-context, and max seq len is 256
    The experimental results are as follows:
  1. Train two different lora total cost time(hours)
    result (手机)
  2. Train two different lora thuoughput(tokens/second)
    result_1 (手机)

About Mix LoRA

I think this is a great job

Is there any problem with the implementation of the method? Why is the code no longer in the warehouse?

Issues about integrated inference

Traceback

Traceback (most recent call last):
  File "/home/mikecovlee/work/multi-lora-fine-tune/mlora.py", line 175, in <module>
    inference(config, model, tokenizer)
  File "/home/mikecovlee/work/multi-lora-fine-tune/mlora.py", line 106, in inference
    input_data = mlora.MultiLoraBatchData(
TypeError: MultiLoraBatchData.__init__() got an unexpected keyword argument 'prompts_'

TODO

Improve inference functions. @mikecovlee

Question about Training

Dear Authors,

Thanks for this great project.
I got a question about training,
I can see this part only produces one work for inference, why are we not using auto-regressive here?
Also, I wondered how we should test the throughput like tokens per second.

Best
Chao
Screenshot 2023-11-01 at 2 01 50 PM

Known Issues

  • ⚠️ Only last layer of adapter will be updated when training
  • Slow build-in inference

Offload Performance Test Result

Test data: batch-size = 4, seqlen = 1552, use vicuna-7B model in one GPU to test.
The vicuna-7B has 32 transformer layers, use checkpoint in each layer.
case1: 31-layer use the recompute checkpoint, 1-layer use the offload checkpoint, time cost: 12.8416s
case2: 32-layer all use the recompute checkpoint, time cost: 11.3163s
It seems if we use offload in one card, it will be 1.135 times slower than recomputing.

Support automatic parameter configuration

Fining tuning multiple lora on a single GPU might encounter OOM issue. It is necessary to carefully adjust parameters such as batch_size and cutoff_len, but this still cannot guarantee to completely avoid OOM. Is it possible to run a tool first to provide a reference(or best) configuration for users based on their data?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.