FasterTransformer 003: CMAKELIST in gemm_test

news2024/10/7 2:19:23
  • cmake -DSM=60 -DCMAKE_BUILD_TYPE=Release ..

  • -DCMAKE_BUILD_TYPE cmake编译类型通常默认为debug,但是在编译软件时,一般都需要使用release版本的,debug太慢了。设置为release版本可以在cmake文件里进行,也可以在运行cmake命令时使用参数进行指定。

  • STREQUAL 用于比较字符串,相同返回 true

  • 找不CUDA编译器,需要设置CUDACXX或CMAKE_CUDA_COMPILER变量,或者增加PATH。

  • CMake Error at CMakeLists.txt:15 (project): No CMAKE_CUDA_COMPILER could be found.

  • SOLVE: Tell CMake where to find the compiler by setting either the environment variable "CUDACXX" or the CMake cache entry CMAKE_CUDA_COMPILER to the full path to the compiler, or to the compiler name if it is in the PATH.
    set(CMAKE_CUDA_COMPILER /usr/local/cuda-11.1/bin/nvcc)
    set(CUDACXX /usr/local/cuda-11.1/bin/nvcc)

fastertransformer

cmake_minimum_required(VERSION 3.8 FATAL_ERROR)

project(FasterTransformer LANGUAGES CXX CUDA)

find_package(CUDA 10.0 REQUIRED)

option(BUILD_TRT "Build in TensorRT mode" OFF)
option(BUILD_TF "Build in TensorFlow mode" OFF)

set(CUDA_PATH ${CUDA_TOOLKIT_ROOT_DIR})

set(TF_PATH "" CACHE STRING "TensorFlow path")
#set(TF_PATH "/usr/local/lib/python3.5/dist-packages/tensorflow")

if(BUILD_TF AND NOT TF_PATH)
  message(FATAL_ERROR "TF_PATH must be set if BUILD_TF(=TensorFlow mode) is on.")
endif()

set(TRT_PATH "" CACHE STRING "TensorRT path")
#set(TRT_PATH "/myspace/TensorRT-5.1.5.0")

if(BUILD_TRT AND NOT TRT_PATH)
  message(FATAL_ERROR "TRT_PATH must be set if BUILD_TRT(=TensorRT mode) is on.")
endif()

list(APPEND CMAKE_MODULE_PATH ${CUDA_PATH}/lib64)
find_package(CUDA REQUIRED)

set (SM 60)


# setting compiler flags
if (SM STREQUAL 70 OR
    SM STREQUAL 75 OR
    SM STREQUAL 61 OR
    SM STREQUAL 60)
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -gencode=arch=compute_${SM},code=\\\"sm_${SM},compute_${SM}\\\" -rdc=true")
  if (SM STREQUAL 70 OR SM STREQUAL 75)
    set(CMAKE_C_FLAGS    "${CMAKE_C_FLAGS}    -DWMMA")
    set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FLAGS}  -DWMMA")
    set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -DWMMA")
  endif()

set(CMAKE_C_FLAGS    "${CMAKE_C_FLAGS}")	
set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FLAGS}")
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS}  -Xcompiler -Wall")
message("-- Assign GPU architecture (sm=${SM})")
else()
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -gencode=arch=compute_60,code=\\\"sm_60,compute_60\\\" -rdc=true")
message("-- Unknown or unsupported GPU architecture (set sm=60)")
endif()
set(CMAKE_C_FLAGS_DEBUG    "${CMAKE_C_FLAGS_DEBUG}    -Wall -O0")
set(CMAKE_CXX_FLAGS_DEBUG  "${CMAKE_CXX_FLAGS_DEBUG}  -Wall -O0")
set(CMAKE_CUDA_FLAGS_DEBUG "${CMAKE_CUDA_FLAGS_DEBUG} -O0 -G -Xcompiler -Wall")


set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(CMAKE_CXX_STANDARD STREQUAL "11")
  set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-extended-lambda")
  set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr")
  set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --std=c++11")
endif()

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -Xcompiler -O3")

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

set(COMMON_HEADER_DIRS
  ${PROJECT_SOURCE_DIR}
  ${CUDA_PATH}/include
)

set(COMMON_LIB_DIRS
  ${CUDA_PATH}/lib64
)

if(BUILD_TF)
  list(APPEND COMMON_HEADER_DIRS ${TF_PATH}/include)
  list(APPEND COMMON_LIB_DIRS ${TF_PATH})
endif()

if(BUILD_TRT)
  list(APPEND COMMON_HEADER_DIRS ${TRT_PATH}/include)
  list(APPEND COMMON_LIB_DIRS ${TRT_PATH}/lib)
endif()

include_directories(
  ${COMMON_HEADER_DIRS}
)

link_directories(
  ${COMMON_LIB_DIRS}
)

add_subdirectory(tools/gemm_test)
add_subdirectory(fastertransformer)
add_subdirectory(sample)


if(BUILD_TF)
  add_custom_target(copy ALL COMMENT "Copying tensorflow test scripts")
  add_custom_command(TARGET copy
      POST_BUILD
      COMMAND cp ${PROJECT_SOURCE_DIR}/sample/tensorflow/*.py ${PROJECT_SOURCE_DIR}/build/
  )
endif()


gemm

  • 修改后能单独编译的cmakelist文件为:
cmake_minimum_required(VERSION 3.8)
set(CMAKE_CUDA_COMPILER /usr/local/cuda-11.1/bin/nvcc)
set(CUDACXX /usr/local/cuda-11.1/bin/nvcc)  # Detecting CXX compile features

project(gemm_test LANGUAGES CXX CUDA)

set(gemm_fp16_files
  gemm_fp16.cu
)

set(gemm_fp32_files  gemm_fp32.cu
)


add_executable(gemm_fp32 ${gemm_fp32_files}) # 生成目标可执行文件
set_target_properties(gemm_fp32 PROPERTIES CUDA_RESOLVE_DEVICE_SYMBOLS ON)
target_link_libraries(gemm_fp32 PUBLIC -lcublas -lcudart ${CMAKE_THREAD_LIBS_INIT})

add_executable(gemm_fp16 ${gemm_fp16_files})
set_target_properties(gemm_fp16 PROPERTIES CUDA_RESOLVE_DEVICE_SYMBOLS ON)
target_link_libraries(gemm_fp16 PUBLIC -lcublas -lcudart ${CMAKE_THREAD_LIBS_INIT})
  • 运行~/test/FastT/FasterTransformer/tools/gemm_test/cmake-build-debug$ ./gemm_fp16 1 128 12 64

代码注释


// 该程序首先包括几个标准C++和CUDA库,以及一个名为“common.h”的自定义头文件。然后,它定义了一个称为“diffTime”的函数,该函数以毫秒为单位计算两个时间戳之间的差。
#include <cstdio>
#include <cstdlib>
#include <cuda_fp16.h>
#include <cuda_profiler_api.h>
#include <ctime>
#include <unistd.h>
#include <sys/time.h>
#include "common.h"
using namespace std;
double diffTime(timeval start, timeval end)
{
  return (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) * 0.001;
}

// 这是一个C++程序,使用CUDA和cuBLAS库测试矩阵乘法运算的性能。该程序创建几个矩阵,并对它们执行不同类型的矩阵乘法,测量每次乘法的执行时间,并为每次乘法选择最快的算法。
int main()
{
  //主函数首先打开一个名为“gemm_config.in”的文件
  FILE* fd = fopen("gemm_config.in", "w");
  if(fd == NULL)
  {
    printf("Cannot write to file gemm_config.in\n");
    return 0;
  }
  struct cudaDeviceProp prop;
  cudaGetDeviceProperties(&prop, 0);
  printf("Device %s\n", prop.name);
  //为矩阵乘法中使用的矩阵的维数设置几个常量。
  const int batch_size = atoi("1");
  const int seq_len = atoi("12");
  const int head_num = atoi("12");
  const int size_per_head = atoi("12");

  // 设置了几个数组来存储关于每个矩阵乘法运算的信息,例如所涉及的矩阵的维数和正在执行的运算的描述。
  const int gemm_num = 5;
  int M[gemm_num];
  int N[gemm_num];
  int K[gemm_num];
  int batchCount[gemm_num] = {1,1,1,1,1};
  char mess[gemm_num][256];
  
  //gemm1 
  M[0] = batch_size * seq_len;
  K[0] = head_num * size_per_head;
  N[0] = K[0];
  strcpy(mess[0], "from_tensor * weightQ/K/V, attr * output_kernel");

  //gemm2
  M[1] = M[0];
  K[1] = K[0];
  N[1] = 4 * N[0];
  strcpy(mess[1], "attr_output * inter_kernel");

  //gemm3
  M[2] = M[0];
  K[2] = 4 * K[0];
  N[2] = N[0];
  strcpy(mess[2], "inter_matmul * output_kernel");

  M[3] = seq_len;
  N[3] = seq_len;
  K[3] = size_per_head;
  batchCount[3] = batch_size * head_num;
  strcpy(mess[3], "attention batched Gemm1");

  M[4] = seq_len;
  N[4] = size_per_head; 
  K[4] = seq_len;
  batchCount[4] = batch_size * head_num;
  strcpy(mess[4], "attention batched Gemm2");

  // 然后,该程序创建一个cuBLAS句柄  cublasHandle_t是表示cuBLAS库的句柄的类型,cuBLAS是基本线性代数子程序(BLAS)库。它提供了在NVIDIA GPU上执行矩阵运算的各种例程。
  // 总之,这两行代码为使用cuBLAS库在NVIDIA GPU上执行矩阵运算奠定了必要的基础设施。
  cublasHandle_t cublas_handle;///usr/local/cuda-11.1/targets/x86_64-linux/include/cublas_api.h
  cublasCreate(&cublas_handle);// cublasCreate(&cublas_handle)初始化cublas库并为其创建一个句柄。该函数将指向cublasHandle_t变量的指针作为参数,并将其设置为指向新创建的cublas句柄。这个句柄可以用来调用cuBLAS库提供的各种矩阵运算。

  typedef __half T;
  cudaDataType_t AType = CUDA_R_16F;
  cudaDataType_t BType = CUDA_R_16F;
  cudaDataType_t CType = CUDA_R_16F;
  cudaDataType_t computeType = CUDA_R_16F;
  const int ites = 100;
  struct timeval start, end;

  // 两行代码定义了要测试的cuBLAS GEMM(通用矩阵乘法)算法的范围。 CUBLAS_GEMM_DEFAULT_TENSOR_OP和CUBLAS-GEMM_ALEGO15_TENSOR_OP是表示不同CUBLAS GEMM算法的常数。
  int startAlgo = (int)CUBLAS_GEMM_DEFAULT_TENSOR_OP;//CUBLAS_GEMM_DEFAULT_TENSOR_OP是指CUBLAS提供的默认算法,该算法针对大多数矩阵大小进行了优化。
  int endAlgo = (int)CUBLAS_GEMM_ALGO15_TENSOR_OP; //CUBLAS_GEMM_ALGO15_TENSOR_OP是指由数字15标识的特定算法,该算法针对中小型矩阵进行了优化。
  // 通过定义一系列要测试的算法,代码可以比较计算中使用的给定矩阵大小的不同算法的性能。函数cublasGemmEx()可以用startAlgo和endAlgo之间的不同算法调用,以测试它们,并确定哪种算法在给定的问题大小下产生最佳性能。

  T alpha = (T)1.0f;
  T beta = (T)0.0f;

  printf("***FP16 Gemm Testing***\n");
  for(int i = 0; i < gemm_num; ++i)
  {
    int m = M[i], n = N[i], k = K[i];
    printf("\n-----------------------------\n");
    printf("GEMM test %d: [M: %d, K: %d, N: %d] %s\n", i, m, k, n, mess[i]);
    T* d_A;
    T* d_B;
    T* d_C;
    check_cuda_error(cudaMalloc((void**)&d_A, sizeof(T) * m * k * batchCount[i]));
    check_cuda_error(cudaMalloc((void**)&d_B, sizeof(T) * k * n * batchCount[i]));
    check_cuda_error(cudaMalloc((void**)&d_C, sizeof(T) * m * n * batchCount[i]));

    float exec_time = 99999.0f;
    int fast_algo = 0;
    for(int algo = startAlgo; algo <= endAlgo; algo++)
    {
      cudaDeviceSynchronize();
      gettimeofday(&start, NULL);
      for(int ite = 0; ite < ites; ++ite)
      {
        if(i < 3)
        {
          check_cuda_error(cublasGemmEx(cublas_handle, 
                CUBLAS_OP_N, CUBLAS_OP_N,
                n, m, k, 
                &alpha, 
                d_B, BType, n, 
                d_A, AType, k, 
                &beta, 
                d_C, CType, n, 
                computeType, 
                static_cast<cublasGemmAlgo_t>(algo)));
        }
        else if(i == 3)
        {
          check_cuda_error(cublasGemmStridedBatchedEx(cublas_handle,
                CUBLAS_OP_T, CUBLAS_OP_N,
                seq_len, seq_len, size_per_head,
                &alpha,
                d_B, BType, size_per_head, seq_len * size_per_head,
                d_A, AType, size_per_head, seq_len * size_per_head,
                &beta,
                d_C, CType, seq_len, seq_len * seq_len,
                batch_size * head_num,
                computeType,
                static_cast<cublasGemmAlgo_t>(algo)));
        }
        else
        {
          check_cuda_error(cublasGemmStridedBatchedEx(cublas_handle,
                CUBLAS_OP_N, CUBLAS_OP_N,
                size_per_head, seq_len, seq_len,
                &alpha,
                d_B, BType, size_per_head, seq_len * size_per_head,
                d_A, AType, seq_len, seq_len * seq_len,
                &beta,
                d_C, CType, size_per_head, seq_len * size_per_head,
                batch_size * head_num,
                computeType,
                static_cast<cublasGemmAlgo_t>(algo)));
        }
      }
      cudaDeviceSynchronize();
      gettimeofday(&end, NULL);
      printf("algo_%d costs %.3fms \n", algo, diffTime(start, end) / ites);
      if(diffTime(start, end) / ites < exec_time)
      {
        exec_time = diffTime(start, end) / ites;
        fast_algo = algo;
      }
    }
    printf("fast_algo %d costs %.3f ms\n", fast_algo, exec_time);
    fprintf(fd, "%d\n", fast_algo);
  }

}



use gemm_config.in file later in “/FasterTransformer/fastertransformer/cuda/open_attention.h”

在这里插入图片描述

if can’t find will use default (中英混合是因为我的linux系统没有安装中文输入法)

在这里插入图片描述

错误与处理

  • Attempt to add link library “-lcublas” to target “gemm_fp32” which is not

  • 可能因为配置出问题了degug时 Error running ‘gemm_fp16’: Cannot run program “cmake_device_link.o” (in directory “/home/pdd/test/FastT/FasterTransformer/tools/gemm_test/cmake-build-debug/CMakeFiles/gemm_fp16.dir”): error=13, 权限不够

  • 但是可以直接运行~/test/FastT/FasterTransformer/tools/gemm_test/cmake-build-debug$ ./gemm_fp16 1 128 12 64

  • 发现找不到bashrc nvcc的路径了 :   / t e s t / F a s t T / F a s t e r T r a n s f o r m e r / t o o l s / g e m m t e s t :~/test/FastT/FasterTransformer/tools/gemm_test : /test/FastT/FasterTransformer/tools/gemmtest nvcc -V
    Traceback (most recent call last):
    File “/usr/lib/command-not-found”, line 27, in
    from CommandNotFound.util import crash_guard
    ModuleNotFoundError: No module named ‘CommandNotFound’

CG

  • 通用矩阵乘(GEMM)优化与卷积计算

  • VIDEO 【HPC 05】CPU 和 CUDA 的 GEMM 实现

  • https://github.com/mrzhuzhe/riven/tree/main/cuda_test

  • https://stackoverflow.com/questions/66327073/how-to-find-and-link-cuda-libraries-using-cmake-3-15

  • add_executable(test benchmark.cpp)
    find_package(CUDALibs)
    target_link_libraries(test CUDA::cudart CUDA::cublas CUDA::cufft CUDA::cusolver CUDA::curand CUDA::nppicc CUDA::nppial CUDA::nppist CUDA::nppidei CUDA::nppig CUDA::nppitc CUDA::npps)

  • A Visual Studio Code extension for building and debugging CUDA applications.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/652157.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习实践篇[17]:模型压缩技术、模型蒸馏算法:Patient-KD、DistilBERT、DynaBERT、TinyBERT

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

观点碰撞燃爆会场,2023开放原子全球开源峰会区块链分论坛圆满落幕!

2023开放原子全球开源峰会区块链分论坛活动现场 6月13日&#xff0c;由开放原子开源基金会主办&#xff0c;XuperCore开源项目承办&#xff0c;北京百度网讯科技有限公司、招商银行、联通数字科技有限公司、杭州趣链科技有限公司等协办的2023开放原子全球开源峰会区块链分论坛在…

《低代码指南》不能“生成代码”的低代码平台,为什么推进阻力那么大?

为什么现在低代码平台推进阻力那么大? “在踏出一步之前,首先考虑能否退回去” 现在低代码平台,功能性能这些先不说,能不能提升效率,提升多少,暂不讨论。光“平台和环境锁定”这一点,就是整个行业最大的技术推广障碍。道理很简单,平台有几百个,但是如果选了一个,…

c++11 标准模板(STL)(std::basic_ios)(一)

定义于头文件 <ios>template< class CharT, class Traits std::char_traits<CharT> > class basic_ios : public std::ios_base 类 std::basic_ios 提供设施&#xff0c;以对拥有 std::basic_streambuf 接口的对象赋予接口。数个 std::basic_ios 对…

汽车IVI中控开发入门及进阶(八):视频相关的一些知识点

前言: 视频最早的渊源来源于电视。做汽车仪表/IVI中控,尤其是IVI信息娱乐部分,都要涉及到视频这个知识点,各种概念很多,首先需要明确一条主线,那就是SDTV标清电视->HDTV高清电视->UHDTV超高清电视的一个发展脉络,BT601/656是SDTV标清电视接口,BT1120则对应HDTV高…

SpringCloud:什么是SpringCloud?①

一、什么是SpringCloud 像“云朵”一样聚集起来管理服务。它的定位就是&#xff1a;服务群组间的通讯管理。 二、Spring&#xff0c;SpringBoot以及SpringCloud之间的关系。 Spring是一种引擎式的底层核心-- bean容器思想。后面均是基于它延伸的高级产品。 SpringBoot 专注单体…

为什么中国大公司不开发 Linux 桌面应用?

我们Linux平台C端的业务&#xff0c;也就是给大家提供的Linux的个人版本。目前真的是靠爱发电了&#xff0c;逃&#xff09; 更爱的是&#xff0c;我们不只是提供了X64平台&#xff0c;更是提供了Arm&#xff0c;MIPS64平台的二进制包。 估计国内在Linux平台首发新功能的桌面应…

马尔科夫模型 详解

马尔可夫性 马尔可夫性也叫做无后效性、无记忆性&#xff0c;即是过去只能影响现在&#xff0c;不能影响将来。 在数学上&#xff0c;如果为一个随机过程&#xff0c;则马科尔夫性质可以符号化成如下形式&#xff1a; 从上式可知&#xff0c;过去(s)并不影响将来(th)的状态&a…

Python数据分析讲课笔记02:Numpy基础

文章目录 零、学习目标一、NumPy概述二、多维数组对象三、创建NumPy多维数组1、利用array()函数创建NumPy数组2、创建NumPy数组的其它函数&#xff08;1&#xff09;利用zeros函数创建元素全为0的数组&#xff08;2&#xff09;利用ones函数创建元素全为1的数组&#xff08;3&a…

时间序列异常检测:统计和机器学习方法介绍

在本文中将探索各种方法来揭示时间序列数据中的异常模式和异常值。 时间序列数据是按一定时间间隔记录的一系列观测结果。它经常在金融、天气预报、股票市场分析等各个领域遇到。分析时间序列数据可以提供有价值的见解&#xff0c;并有助于做出明智的决策。 异常检测是识别数…

增加内容曝光、获得更多粉丝 - 「评论发红包」功能

目录 博客发放以及领取红包规则 1. 发布博客评论社区红包规则&#xff1a; 2. 博客评论红包领取规则 如何发红包评论&#xff1f; 发布红包评论益处 不知道大家有没有注意到&#xff0c;我们的「评论发红包」功能已经上线啦&#xff5e; 现在几乎所有的内容 -- 博客&…

管理类联考——英语——技巧篇——完型填空——经典方法论

放弃完型填空&#xff0c;意味着你的阅读部分得分至少能稳定在34分以上(满分40分&#xff0c;要得34分以上意味着至多只能错3道题)&#xff0c;且其他题型也发挥不错&#xff0c;才能确保总分是一个不错的分数。然而&#xff0c;翻译和写作考查的都是硬实力&#xff0c;新题型自…

解释器模式(二十三)

相信自己&#xff0c;请一定要相信自己 上一章简单介绍了 备忘录模式(二十二), 如果没有看过, 请观看上一章 一. 解释器模式 引用 菜鸟教程里面 解释器模式介绍: https://www.runoob.com/design-pattern/interpreter-pattern.html 解释器模式&#xff08;Interpreter Patter…

科研热点|影响因子发布时间确定,AHCIESCI将首获IF~

根据科睿唯安&#xff08;Clarivate&#xff09;官方公众号消息&#xff0c;2023年度《期刊引证报告》&#xff08;Journal Citation Reports&#xff0c;简称JCR&#xff09;即将于今年6月底正式发布&#xff01; 本年度JCR将对Web of Science核心合集收录的所有期刊赋予期刊…

C语言指针讲解(适用于初学者)

本文参考视频: https://b23.tv/xLOG6SV,相当于学习笔记&#xff0c;这样概念混淆的时候也可以看看。 一.一级指针 以下图表示的意思是&#xff1a; a的地址为0XA0&#xff0c;定义一个指针p&#xff0c;指向a的地址&#xff0c;计算机也会给p一个内存空间&p:0XB0 图中&a…

【机器学习】十大算法之一 “KNN”

作者主页&#xff1a;爱笑的男孩。的博客_CSDN博客-深度学习,活动,python领域博主爱笑的男孩。擅长深度学习,活动,python,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typeblog个…

疑似有用户安装Win11六月更新之后,无法打开Chrome浏览器

近日有网友表示&#xff0c;在安装6月更新的KB5027231之后&#xff0c;无法打开Chrome浏览器了&#xff0c;并且在任务管理器中可以看到相关**ERP系统**进程&#xff0c;但是 Chrome浏览器无法显示。 据了解&#xff0c;微软在本月的补丁星期二活动中&#xff0c;面向Win11发布…

MySQL:多表查询(全面详解)

MySQL&#xff1a;多表查询 前言附录&#xff1a;常用的 SQL 标准有哪些一、一个案例引发的多表连接1、案例说明2、笛卡尔积&#xff08;或交叉连接&#xff09;的理解3、案例分析与问题解决 二、多表查询分类讲解1、等值连接 vs 非等值连接1.1 等值连接1.2 非等值连接 2、自连…

chatgpt赋能python:使用Python生成应用的SEO

使用Python生成应用的SEO 在当今数字化时代中&#xff0c;拥有一个优化良好的应用程序对于任何企业都是至关重要的。 SEO&#xff08;搜索引擎优化&#xff09;是一个崭新且不断发展的领域&#xff0c;它对于企业非常具有建设性。在本文中&#xff0c;我们将学习如何使用Pytho…

CADisplayLink前世今生

本文字数&#xff1a;19803字 预计阅读时间&#xff1a;50分钟 用最通俗的语言&#xff0c;描述最难懂的技术 前情描述 上周同事做code review的时候说到了CADisplayLink的一些变化&#xff0c;感触颇深&#xff0c;提到了接口的一些变动&#xff0c;现在就自己的一些理解加上网…