CUDA从入门到精通(六)——CUDA编程模型(二)

打印 上一主题 下一主题

主题 833|帖子 833|积分 2501

1. 核函数范例限定符

CUDA 核函数的常用函数范例限定符及其相关信息的表格:
限定符执行端调用方式备注__global__装备端(GPU)从主机代码使用 <<<...>>> 调用核函数用于声明核函数,在 GPU 上执行。只能从主机代码调用。通常没有返回值。__device__装备端(GPU)只能从装备代码(核函数或其他装备函数)调用用于声明装备函数,只能在 GPU 上执行,不能从主机代码调用。__host__主机端(CPU)只能从主机代码调用用于声明主机函数,必须在 CPU 上执行,不能从装备代码调用。__host__ __device__主机端(CPU)和装备端(GPU)可以从主机或装备代码调用该函数可以在主机和装备上执行,适用于需要兼容主机和装备的通用函数。__launch_bounds__装备端(GPU)用于核函数声明用于提示编译器优化线程块的巨细和寄存器的使用。__restrict__装备端(GPU)用于指针参数声明用于声明指针,告诉编译器该指针所指向的内存不会被其他指针修改,有助于性能优化。

  • 具体说明:

  • __global__

    • 核函数限定符,表示该函数是由 GPU 上的线程执行的。
    • 从主机代码中调用,使用 <<<...>>> 语法举行配置。

  • __device__

    • 用于声明装备函数,函数仅在 GPU 代码中执行。
    • 只能被核函数或其他装备函数调用,无法从主机代码直接调用。

  • __host__

    • 用于声明主机函数,表示该函数只能在 CPU 上执行。
    • 只能从主机代码中调用,不能从装备代码中调用。

  • __host__ __device__

    • 允许函数在主机和装备上都执行,兼容两头的调用。
    • 适用于那些通用的函数,它们可以同时在主机和装备上执行。

  • __launch_bounds__

    • 用于优化核函数的执行,提供线程块巨细和寄存器使用的提示。
    • 提示编译器对核函数的线程调理举行优化。

  • __restrict__

    • 用于指针范例,告知编译器该指针所指向的内存不会被其他指针修改。
    • 允许编译器举行更有效的优化,淘汰内存访问辩论。

在 CUDA 编程中,核函数(kernel functions)是由 GPU 上的线程执行的函数。只管 CUDA 提供了强盛的并行计算能力,但在使用核函数时也存在一些限制。以下是一些重要的限制:
2. 核函数限制

1. 返回值限制



  • 核函数不能返回值:核函数的返回范例必须是 void,因为它们不能直接返回值。所有的结果必须通过指针或引用转达回主机。
2. 线程和块的限制



  • 最大线程数:每个线程块的最大线程数通常为 1024(具体取决于 GPU 架构)。这意味着在一个线程块中,您不能创建超过这个数目的线程。
  • 最大线程块数:每个网格的最大线程块数也有限制,具体取决于 GPU 的计算能力。
  • 线程块维度:线程块的维度(即线程的数目)通常限制为 1D、2D 或 3D,且每个维度的巨细也有上限。
3. 内存限制



  • 共享内存限制:每个线程块可以使用的共享内存量是有限的,通常为 48KB(具体取决于 GPU 架构)。如果需要更多的共享内存,大概需要调解线程块的巨细。
  • 全局内存访问延迟:虽然全局内存可以存储大量数据,但访问全局内存的延迟相对较高。频繁的全局内存访问大概会导致性能降落。
4. 装备函数限制



  • 装备函数不能被主机代码调用:装备函数(使用 __device__ 限定符声明的函数)只能在装备代码中调用,不能从主机代码直接调用。
5. 递归限制



  • 不支持递归:CUDA 核函数不支持递归调用。所有的函数调用必须是非递归的。
6. 线程同步限制



  • 线程同步:在同一个线程块内,可以使用 __syncthreads() 举行线程同步,但不能跨线程块举行同步。跨块的同步需要其他机制,如原子操纵或全局内存的和谐。
7. 装备属性限制



  • 装备属性:不同的 GPU 装备具有不同的计算能力和资源限制。开发者需要根据目的装备的属性举行优化。
8. 装备内存分配限制



  • 动态内存分配:在核函数中使用动态内存分配(如 malloc)是有限制的,大概会导致性能降落。动态分配的内存也大概会导致内存碎片。
9. 计算能力限制



  • 计算能力:不同的 GPU 具有不同的计算能力(如 CUDA 计算能力 2.0、3.0、5.0 等),某些功能和特性大概在较低的计算能力下不可用。
10. 装备和主机之间的数据传输



  • 数据传输开销:在主机和装备之间传输数据(如从主机到装备的内存拷贝)会引入开销,频繁的数据传输会影响性能。
3.核函数计时

在 CUDA 编程中,计时核函数的执行时间是评估性能的重要步骤。可以使用 CUDA 提供的事件(events)来精确测量核函数的执行时间。以下是实现核函数计时的步骤和示例代码。
1. 使用 CUDA 事件计时

CUDA 事件是用于测量时间的高精度工具。通过创建事件并在核函数执行前后纪录时间,可以计算出核函数的执行时间。

  • 创建事件:使用 cudaEventCreate() 创建事件。
  • 纪录事件:在核函数调用前后使用 cudaEventRecord() 纪录事件。
  • 计算时间:使用 cudaEventElapsedTime() 计算两个事件之间的时间差。
  • 清理事件:使用 cudaEventDestroy() 清理事件。
  1. #include <iostream>
  2. #include <cuda_runtime.h>
  3. __global__ void kernel_function() {
  4.     // 核函数代码
  5.     int idx = threadIdx.x + blockIdx.x * blockDim.x;
  6.     // 进行一些计算 if (idx < 1000) {
  7.         // 示例计算
  8.         float value = idx * 2.0f;
  9.     }
  10. }
  11. int main() {
  12.     // 创建 CUDA 事件
  13.     cudaEvent_t start, stop;
  14.     cudaEventCreate(&start);
  15.     cudaEventCreate(&stop);
  16.     // 设置线程块和网格大小
  17.     int blockSize = 256;
  18.     int numBlocks = (1000 + blockSize - 1) / blockSize;
  19.     // 记录开始事件
  20.     cudaEventRecord(start);
  21.     // 调用核函数 kernel_function<<<numBlocks, blockSize>>>();
  22.     // 记录结束事件 cudaEventRecord(stop);
  23.     // 等待事件完成
  24.     cudaEventSynchronize(stop);
  25.     // 计算时间 float milliseconds = 0;
  26.     cudaEventElapsedTime(&milliseconds, start, stop);
  27.     // 输出执行时间
  28.     std::cout << "Kernel execution time: " << milliseconds << " ms" << std::endl;
  29.     // 清理事件
  30.     cudaEventDestroy(start);
  31.     cudaEventDestroy(stop);
  32.     return 0;
  33. }
复制代码

  • 核函数:kernel_function 是一个简单的核函数,执行一些计算。
  • 事件创建:使用 cudaEventCreate() 创建 start 和 stop 事件。
  • 纪录事件

    • 在调用核函数之前,使用 cudaEventRecord(start) 纪录开始时间。
    • 在核函数调用之后,使用 cudaEventRecord(stop) 纪录结束时间。

  • 同步事件:使用 cudaEventSynchronize(stop) 确保核函数执行完成。
  • 计算时间:使用 cudaEventElapsedTime(&milliseconds, start, stop) 计算两个事件之间的时间差,单元为毫秒。
  • 输出时间:输出核函数的执行时间。
  • 清理事件:使用 cudaEventDestroy() 清理事件,释放资源。


  • CUDA 装备同步:在纪录结束事件后,确保使用 cudaEventSynchronize() 等候核函数完成,以获得正确的时间。
  • 错误检查:在实际应用中,建议在每个 CUDA API 调用后添加错误检查,以确保没有发生错误。
  • 多次测量:为了获得更稳定的性能数据,可以多次运行核函数并计算平均时间。
除了使用 CUDA 提供的 硬件性能计数器(如 CPI计时器)外,您还可以基于 CPU计时器nvprof 工具举行核函数执行时间的计时。下面我会具体介绍这两种方法。
2. 基于 CPU 计时器计时

虽然 CUDA 核函数运行在 GPU 上,但我们仍然可以使用 CPU计时器 来测量 CUDA 程序的执行时间,尤其是对核函数调用前后以及数据传输的时间举行测量。常用的 CPU 计时器有 std::chrono 和 clock(),它们可以用于测量 CPU 时间。

  • 使用 std::chrono 计时(C++11 或更高版本)
std::chrono 是 C++11 引入的时间库,提供高精度计时器,可以用来精确地测量 CUDA 核函数的执行时间。std::chrono::high_resolution_clock 是一个高精度时钟,它提供了较高的时间分辨率。
  1. #include <iostream>
  2. #include <chrono>
  3. #include <cuda_runtime.h>
  4. __global__ void kernel_function() {
  5.     int idx = threadIdx.x + blockIdx.x * blockDim.x;
  6.     // 核函数中进行一些计算
  7.     if (idx < 1000) {
  8.         float value = idx * 2.0f;
  9.     }
  10. }
  11. int main() {
  12.     // 使用 std::chrono 高精度计时器
  13.     auto start = std::chrono::high_resolution_clock::now();
  14.     // 设置线程块和网格大小
  15.     int blockSize = 256;
  16.     int numBlocks = (1000 + blockSize - 1) / blockSize;
  17.     // 调用核函数
  18.     kernel_function<<<numBlocks, blockSize>>>();
  19.     // 等待核函数执行完毕
  20.     cudaDeviceSynchronize();
  21.     // 记录结束时间
  22.     auto end = std::chrono::high_resolution_clock::now();
  23.     // 计算执行时间
  24.     std::chrono::duration<float> duration = end - start;
  25.     std::cout << "Kernel execution time: " << duration.count() << " seconds." << std::endl;
  26.     return 0;
  27. }
复制代码


  • std::chrono::high_resolution_clock::now():用于获取当前的时间戳,具有较高的时间精度。
  • cudaDeviceSynchronize():确保核函数执行完毕后再计算时间。
  • duration.count():获取执行的时间,单元是秒。
这种方法适用于需要在 主机端(CPU)计时 CUDA 核函数的场景,但需要留意的是,它只能计时核函数的总执行时间,不能提供 GPU 上具体的硬件性能数据。
3. 使用 nvprof 计时

nvprof 是 NVIDIA Profiler,一个命令行工具,可以大概提供丰富的性能分析数据,帮助你了解 CUDA 程序的执行情况,包罗内存传输、核函数执行时间、硬件性能计数等。使用 nvprof,你可以轻松地获取核函数的执行时间和其他性能指标。
使用 nvprof 计时
nvprof 可以用来纪录 CUDA 核函数的执行时间、内存传输情况以及硬件级别的性能指标(如执行周期、指令数等)。它是 NVIDIA Profiler 工具的一部分,非常适用于性能分析。

  • 编译 CUDA 程序
    起首,编译您的 CUDA 程序,确保使用了调试信息(-g 标记)。比方:
    1. nvcc -g -G -o my_program my_program.cu
    复制代码
  • 运行 nvprof
    使用 nvprof 命令运行您的 CUDA 程序并获取核函数执行时间:
    1. nvprof --metrics time_elapsed ./my_program
    复制代码
    这将显示核函数的执行时间(单元为微秒)。
  • 获取更多性能指标
    nvprof 还可以显示有关硬件资源的其他信息,如执行周期数、指令数等。您可以通过 --metrics 选项获取多个指标:
    1. nvprof --metrics sm__cycles_elapsed.avg,sm__inst_executed.avg ./my_program
    复制代码

    • sm__cycles_elapsed.avg:执行的平均周期数。
    • sm__inst_executed.avg:执行的平均指令数。

  • 获取具体核函数的时间
    如果只关注某个特定的核函数,您可以使用以下命令:
    1. nvprof --kernel <kernel_name> --metrics time_elapsed ./my_program
    复制代码
    其中 <kernel_name> 替换为您程序中核函数的名称。
  1. ==12345== Profiling application: ./my_program
  2. ==12345== Metrics result:
  3. ==12345==   Metric 'time_elapsed' is 1500.0 ms
  4. ==12345==   Metric 'sm__cycles_elapsed.avg' is 2000000
  5. ==12345==   Metric 'sm__inst_executed.avg' is 1000000
复制代码
4. 计算 CPI

如前所述,CPI(Cycles Per Instruction)可以通过以下公式计算:
                                         CPI                            =                                       sm__cycles_elapsed.avg                               sm__inst_executed.avg                                            \text{CPI} = \frac{\text{sm\_\_cycles\_elapsed.avg}}{\text{sm\_\_inst\_executed.avg}}                     CPI=sm__inst_executed.avgsm__cycles_elapsed.avg​
在上面的例子中:


  • sm__cycles_elapsed.avg = 2000000
  • sm__inst_executed.avg = 1000000
所以:
                                         CPI                            =                                       2000000                               1000000                                      =                            2.0                                  \text{CPI} = \frac{2000000}{1000000} = 2.0                     CPI=10000002000000​=2.0
这意味着每条指令在该核函0数执行中平均斲丧 2 个周期。


  • nvprof 提供了具体的性能数据,包罗内存传输、核函数执行时间、硬件资源使用等。
  • nvprof 可以用于查看整个程序的性能,方便发现瓶颈。
  • nvprof 重要是一个命令行工具,不得当与程序中的计时逻辑精密结合。
  • 它通常用来举行后期的分析,而不是实时计时。
方法优点缺点基于 CPU 计时器(如 std::chrono)简单易用,适用于对 CUDA 核函数举行快速计时只能测量核函数的总执行时间,无法提供硬件级别的性能数据基于 nvprof 工具计时提供具体的性能分析数据,支持多种硬件级别的计数器指标(如执行周期、指令数等)重要是后期分析工具,不得当嵌入程序中实时计时,且有额外的运行开销 选择哪种计时方式取决于您的需求:


  • CPU计时器 更适用于简单的性能测量和快速开发。
  • nvprof 得当需要深入了解程序性能和瓶颈的情况,特别是在大规模程序调优时。
    在 CUDA 编程中,网格(grid)和线程块(block)的配置对性能有显著影响。不同的网格和块数目会导致不同的性能体现,重要原因包罗以下几个方面:
4. 不同的线程数目和块数拥有不同的性能

1. 资源使用率



  • GPU 资源限制:每个 GPU 有其特定的资源限制,包罗每个线程块的最大线程数、共享内存、寄存器等。选择符合的线程块巨细可以确保 GPU 资源的高效使用。
  • 并行度:如果线程块数目过少,大概无法充分使用 GPU 的并行计算能力。相反,如果线程块数目过多,大概会导致资源竞争,降低性能。
2. 线程调理



  • 线程块调理:GPU 使用线程调理器来管理线程块的执行。线程块的数目和巨细会影响调理的服从。较小的线程块大概导致调理开销增加,而较大的线程块大概会导致资源浪费。
  • 活跃线程数:为了保持 GPU 的高效运行,通常需要有富足数目的活跃线程。如果线程块数目不足,大概会导致 GPU 处于空闲状态,降低整体性能。
3. 内存访问模式



  • 内存访问服从:线程块的配置会影响内存访问模式。公道的线程块巨细可以提高内存访问的局部性,淘汰全局内存访问的延迟。
  • 共享内存的使用:如果线程块的巨细得当,可以使用共享内存来淘汰全局内存访问,从而提高性能。过小的线程块大概无法有效使用共享内存。
4. 计算与内存传输的平衡



  • 计算与内存传输的比例:在 CUDA 程序中,计算和内存传输是两个重要的性能瓶颈。公道配置网格和块的数目可以帮助平衡计算和内存传输的比例,淘汰内存传输的影响。
  • 内存带宽:如果线程块数目过多,大概会导致内存带宽的竞争,影响性能。得当的块数目可以帮助优化内存带宽的使用。
5. 线程块的巨细



  • 线程块的维度:线程块的维度(1D、2D、3D)也会影响性能。某些算法在特定维度上体现更好,公道选择线程块的维度可以提高性能。
  • 线程块的巨细:较大的线程块大概会导致更多的寄存器和共享内存的使用,影响其他线程块的调理。较小的线程块大概会导致调理开销增加。
6. 装备特性



  • GPU 架构:不同的 GPU 架构对线程块和网格的支持不同。某些架构大概对特定的线程块巨细和数目有更好的优化。
  • 计算能力:GPU 的计算能力(如 CUDA 计算能力)会影响可用的资源和性能体现。了解目的装备的特性可以帮助优化网格和块的配置。
7. 负载平衡



  • 负载平衡:公道的网格和块配置可以确保每个线程块的工作量相对匀称,避免某些线程块过载而其他线程块空闲的情况。负载不平衡会导致性能降落。
不同的网格和块数目会影响 CUDA 程序的性能,重要是因为它们影响了资源使用率、线程调理、内存访问模式、计算与内存传输的平衡、线程块的巨细、装备特性和负载平衡等因素。为了获得最佳性能,开发者需要根据具体的应用场景和目的 GPU 的特性,公道配置网格和块的数目。通常,举行性能测试和基准测试是找到最佳配置的有效方法。
5. 装备管理

在 CUDA 编程中,查询 GPU 装备信息、选择最佳 GPU 装备并举行装备管理是性能优化的重要步骤。以下是怎样使用不同的 API 查询装备信息,选择最佳 GPU,使用 nvidia-smi
查询 GPU 信息以及在运行时设置装备的具体方法。
1. 使用 CUDA API 查询装备信息

CUDA 提供了多个 API 函数来查询 GPU 装备的各种信息,如装备数目、属性、内存、计算能力等。
1.1 查询装备数目

  1. #include <iostream>
  2. #include <cuda_runtime.h>
  3. int main() {
  4.     int deviceCount;
  5.     cudaError_t err = cudaGetDeviceCount(&deviceCount);
  6.    
  7.     if (err != cudaSuccess) {
  8.         std::cerr << "Error getting device count: " << cudaGetErrorString(err) << std::endl;
  9.         return -1;
  10.     }
  11.     std::cout << "Number of CUDA devices: " << deviceCount << std::endl;
  12.     return 0;
  13. }
复制代码


  • cudaGetDeviceCount(&deviceCount):返回可用的 CUDA 装备数目。
1.2 获取装备属性

每个 CUDA 装备都有一个 cudaDeviceProp 结构体,包罗装备的各种信息。比方,内存巨细、计算能力、每个线程块的最大线程数等。
  1. #include <iostream>
  2. #include <cuda_runtime.h>
  3. void printDeviceProperties(int deviceId) {
  4.     cudaDeviceProp prop;
  5.     cudaGetDeviceProperties(&prop, deviceId);
  6.    
  7.     std::cout << "Device " << deviceId << ": " << prop.name << std::endl;
  8.     std::cout << "  Total Global Memory: " << prop.totalGlobalMem / (1024 * 1024) << " MB" << std::endl;
  9.     std::cout << "  Shared Memory per Block: " << prop.sharedMemPerBlock / 1024 << " KB" << std::endl;
  10.     std::cout << "  Max Threads per Block: " << prop.maxThreadsPerBlock << std::endl;
  11.     std::cout << "  Compute Capability: " << prop.major << "." << prop.minor << std::endl;
  12. }
  13. int main() {
  14.     int deviceCount;
  15.     cudaGetDeviceCount(&deviceCount);
  16.     for (int i = 0; i < deviceCount; ++i) {
  17.         printDeviceProperties(i);
  18.     }
  19.     return 0;
  20. }
复制代码


  • cudaGetDeviceProperties(&prop, deviceId):查询指定装备的属性,存储在 cudaDeviceProp 结构体中。
  • prop.name:装备名称。
  • prop.totalGlobalMem:装备的全局内存总量(以字节为单元)。
  • prop.computeCapability:计算能力(如 6.1 表示 CUDA 6.1)。
1.3 获取当前装备

使用 cudaGetDevice() 可以获取当前选择的装备。
  1. int currentDevice;
  2. cudaGetDevice(&currentDevice);
  3. std::cout << "Current device is: " << currentDevice << std::endl;
复制代码
1.4 设置装备

使用 cudaSetDevice() 可以在程序中选择要使用的 GPU 装备。
  1. int deviceId = 1;  // 假设选择设备 1
  2. cudaSetDevice(deviceId);
复制代码
2. 选择最佳 GPU 装备

选择最佳 GPU 装备通常基于多个因素,如内存巨细、计算能力、使用的应用场景等。你可以选择具有最大内存或最高计算能力的装备。
比方,以下代码选择具有最大全局内存的装备作为最佳装备:
  1. int bestDevice = 0;
  2. size_t maxMemory = 0;
  3. int deviceCount;
  4. cudaGetDeviceCount(&deviceCount);
  5. for (int i = 0; i < deviceCount; ++i) {
  6.     cudaDeviceProp prop;
  7.     cudaGetDeviceProperties(&prop, i);
  8.    
  9.     if (prop.totalGlobalMem > maxMemory) {
  10.         maxMemory = prop.totalGlobalMem;
  11.         bestDevice = i;
  12.     }
  13. }
  14. std::cout << "Best device is: " << bestDevice << " with " << maxMemory / (1024 * 1024) << " MB memory." << std::endl;
  15. // 选择最佳设备
  16. cudaSetDevice(bestDevice);
复制代码
3. 使用 nvidia-smi
查询 GPU 信息


nvidia-smi
是 NVIDIA 提供的一个命令行工具,用于查询 GPU 状态和管理 GPU 资源。你可以通过 nvidia-smi
查看 GPU 的具体信息,如 GPU 使用情况、温度、内存使用量等。
3.1 查询 GPU 状态

在命令行中使用 nvidia-smi
查询 GPU 状态:
  1. nvidia-smi
复制代码
输出示例:
  1. +-----------------------------------------------------------------------------+
  2. | NVIDIA-SMI 460.32.03    Driver Version: 460.32.03    CUDA Version: 11.2     |
  3. |-------------------------------+----------------------+----------------------+
  4. | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
  5. | Fan  Temp  Perf  Pwr:Usage/Cap| Memory-Usage | GPU-Util  Compute M. |
  6. |===============================+======================+======================|
  7. |   0  Tesla K80           Off  | 00000000:00:1E.0 Off |                    0 |
  8. | N/A   39C    P8    29W / 149W |    0MiB / 11441MiB |      0%      Default |
  9. +-------------------------------+----------------------+----------------------+
复制代码
3.2 查询特定 GPU 的信息

你也可以查询特定 GPU 的信息,比方,查询装备 ID 为 0 的 GPU:
  1. nvidia-smi
  2. -i 0
复制代码
3.3 查看 GPU 内存和使用情况

要查看 GPU 的内存使用情况:
  1. nvidia-smi
  2. --query-gpu=memory.used,memory.free,memory.total --format=csv
复制代码
输出示例:
  1. memory.used [MiB], memory.free [MiB], memory.total [MiB]
  2. 0 MiB, 11264 MiB, 11441 MiB
复制代码
3.4 使用 nvidia-smi
执行任务


您还可以使用 nvidia-smi
在命令行中启动或制止 GPU 任务。比方,查看 GPU 使用情况并限制其计算任务:
  1. nvidia-smi
  2. -i 0 --persistence-mode=1
复制代码
4. 运行时设置装备

CUDA 允许在运行时动态选择 GPU 装备。可以通过以下步骤在应用中举行装备选择:
4.1 获取装备数目

通过 cudaGetDeviceCount() 获取当前系统中的可用 GPU 数目。
4.2 根据装备特性选择装备

根据装备的性能指标(如计算能力、内存巨细等),选择最佳的 GPU。
4.3 设置装备

通过 cudaSetDevice(deviceId) 选择指定的 GPU 装备举行计算。
4.4 同步装备

如果您的程序在多个装备上并行执行,您可以使用 cudaDeviceSynchronize() 来同步装备的执行,确保当前装备的所有任务完成后才举行下一步操纵。
功能CUDA API命令行工具 (nvidia-smi
)查询可用装备数目cudaGetDeviceCount(&deviceCount)N/A查询装备属性cudaGetDeviceProperties(&prop, deviceId)N/A获取当前装备 IDcudaGetDevice(&currentDevice)N/A选择装备cudaSetDevice(deviceId)N/A查询装备内存和使用情况N/Anvidia-smi
--query-gpu=memory.used,memory.free,memory.total --format=csv获取装备信息cudaGetDeviceProperties()nvidia-smi
装备信息过滤cudaDeviceGetAttribute()(如最大线程数、内存等)nvidia-smi
-i <device_id> 通过结合使用 CUDA API 和 nvidia-smi
,可以灵活地查询和选择 GPU 装备,在程序运行时举行装备管理和优化。这有助于提高程序的性能,尤其在多 GPU 系统中。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

刘俊凯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表