【CUDA】 矩陣乘法 matMatMul

矩陣乘法 matMatMul

矩陣乘法是基本線性代數子程序(BLAS)的重要組成部分,而且線性代數中許多其他操作以此為基礎。

圖1是兩個矩陣的乘法。

基礎方法,正方形tile和長方形tile

基礎方法

執行矩陣乘法的基礎方法是使用單個線程執行輸出矩陣的單個元素乘法。這意味著所需的線程數等于輸出矩陣中的元素數。線程排列在二維網格中,每個線程分配一個唯一的索引。線程的索引用于訪問輸入矩陣的相應行和列。執行行和列的乘法,結果存儲在輸出矩陣的相應元素中。

這種方法的主要缺點是多個線程加載相同的行和列來計算它們的輸出。圖2是一個示例。

圖2

正如上圖所示,為了計算P0,0和P0,1,兩個線程都需要加載整個M0。對于P0,0和P1,0也是如此。兩個線程都需要加載整個N1列。這意味著線程將多次訪問相同的內存位置。

正方形tile

為了避免這個問題,我們可以使用tile。tile是將輸入矩陣的一個小部分加載到共享內存中。線程將把tile加載到共享內存中,然后執行乘法運算。圖3描述了這種技術。

圖3

kernel將計算分為幾個階段。每個階段,線程將將M和N矩陣中的tile加載到共享內存中。然后,線程將執行tile的乘法,并將結果累積到輸出矩陣的相應元素中。

通過這種技術,我們可以看到全局內存訪問減少了TILE_WIDTH(圖示)倍。

長方形tile

為了進一步減少全局內存訪問,我們可以使用圖4所示的矩形塊。

圖4

通過這種技術,我們增加了每個線程的工作量,以進一步減少全局內存訪問的次數。kernel再次將計算分成多個階段。在每個階段中,線程將從中加載一個tile M 和兩個tile N 到共享內存中。然后線程將對這些tile進行乘法運算,并將結果累加到輸出矩陣的相應元素中。


Code

host代碼使用隨機值初始化輸入矩陣,并調用kernel執行乘法運算。輸入矩陣以線性格式存儲。


#include <iostream>#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/transform.h>#include "mat_mat_mul_kernels.cuh"int main(int argc, char *argv[])
{int rows1, cols1rows2, cols2, t_x;if(argc != 5){std::cout << "Usage: " << argv[0] << " <rows1> <cols1rows2> <cols2> <t_x>" << std::endl;return 1;}rows1 = atoi(argv[1]);cols1rows2 = atoi(argv[2]);cols2 = atoi(argv[3]);t_x = atoi(argv[4]);thrust::host_vector<float> h_in_mat1(rows1 * cols1rows2);thrust::host_vector<float> h_in_mat2(cols1rows2 * cols2);thrust::host_vector<float> h_out_host(rows1 * cols2);srand(time(NULL));for(int i = 0; i < rows1 * cols1rows2; i++)h_in_mat1[i] = rand() / (float)RAND_MAX;for(int i = 0; i < cols1rows2 * cols2; i++)h_in_mat2[i] = rand() / (float)RAND_MAX;for(int i = 0; i < rows1; ++i)for(int j = 0; j < cols2; ++j)for(int k = 0; k < cols1rows2; ++k)h_out_host[i * cols2 + j] += h_in_mat1[i * cols1rows2 + k] * h_in_mat2[k * cols2 + j];thrust::device_vector<float> d_in_mat1 = h_in_mat1;thrust::device_vector<float> d_in_mat2 = h_in_mat2;thrust::device_vector<float> d_out(rows1 * cols2);dim3 blockDim(t_x, t_x);dim3 gridDim((cols2 + blockDim.x - 1) / blockDim.x, (rows1 + blockDim.y - 1) / blockDim.y);mat_mat_mul<float><<<gridDim, blockDim>>>(thrust::raw_pointer_cast(d_in_mat1.data()),thrust::raw_pointer_cast(d_in_mat2.data()),thrust::raw_pointer_cast(d_out.data()),rows1, cols1rows2, cols1rows2, cols2);bool success = true;for(int i = 0; i < rows1 * cols2; ++i)if(abs(h_out_host[i] - d_out[i]) >= 0.001){std::cout << "Error at " << i << ": " << h_out_host[i] << " != " << d_out[i] << " (Mat Mat Mul)" << std::endl;success = false;break;}if(success)std::cout << "Success (Mat Mat Mul)" << std::endl;blockDim = dim3(t_x, t_x);gridDim = dim3((cols2 + blockDim.x - 1) / blockDim.x, (rows1 + blockDim.y - 1) / blockDim.y);mat_mat_mul_tiles<float><<<gridDim, blockDim, 2 * blockDim.x * blockDim.x * sizeof(float)>>>(thrust::raw_pointer_cast(d_in_mat1.data()),thrust::raw_pointer_cast(d_in_mat2.data()),thrust::raw_pointer_cast(d_out.data()),rows1, cols1rows2, cols1rows2, cols2);success = true;for(int i = 0; i < rows1 * cols2; ++i)if(abs(h_out_host[i] - d_out[i]) >= 0.001){std::cout << "Error at " << i << ": " << h_out_host[i] << " != " << d_out[i] << " (Mat Mat Mul Tiles)" << std::endl;success = false;break;}if(success)std::cout << "Success (Mat Mat Mul Tiles)" << std::endl;blockDim = dim3(t_x, t_x);gridDim = dim3((cols2 + blockDim.x - 1) / blockDim.x / 2, (rows1 + blockDim.y - 1) / blockDim.y);mat_mat_mul_rec_tiles<float><<<gridDim, blockDim, 3 * blockDim.x * blockDim.x * sizeof(float)>>>(thrust::raw_pointer_cast(d_in_mat1.data()),thrust::raw_pointer_cast(d_in_mat2.data()),thrust::raw_pointer_cast(d_out.data()),rows1, cols1rows2, cols1rows2, cols2);success = true;for(int i = 0; i < rows1 * cols2; ++i)if(abs(h_out_host[i] - d_out[i]) >= 0.001){std::cout << "Error at " << i << ": " << h_out_host[i] << " != " << d_out[i] << " (Mat Mat Mul Rec Tiles)" << std::endl;success = false;break;}if(success)std::cout << "Success (Mat Mat Mul Rec Tiles)" << std::endl;return 0;}

以下是kernel的展示。

基礎方法

template<typename T> __global__
void mat_mat_mul(T *in_mat1, T *in_mat2, T *out_mat, int mat1_rows, int mat1_cols, int mat2_rows, int mat2_cols) {int row = blockIdx.y * blockDim.y + threadIdx.y;int col = blockIdx.x * blockDim.x + threadIdx.x;if (row >= mat1_rows || col >= mat2_cols) return;T sum = 0;for (int k = 0; k < mat1_cols; ++k)sum += in_mat1[row * mat1_cols + k] * in_mat2[k * mat2_cols + col];out_mat[row * mat2_cols + col] = sum;
}

在這種基本方法中,kernel非常簡單。

線程首先計算它們的行和列索引。如果行或列索引大于輸入矩陣的行數或列數,則線程返回。

int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;if (row >= mat1_rows || col >= mat2_cols) return;

然后,這些線程會對行和列進行相乘,并將結果存儲在輸出矩陣的相應元素中。

T sum = 0;
for (int k = 0; k < mat1_cols; ++k)sum += in_mat1[row * mat1_cols + k] * in_mat2[k * mat2_cols + col];out_mat[row * mat2_cols + col] = sum;

我們可以觀察到,線程不僅會多次加載相同的行和列,而且它們還會以非合并的方式加載M的元素。這意味著線程將無法充分利用內存帶寬。


正方形tile

template<typename T> __global__
void mat_mat_mul_tiles(T *in_mat1, T *in_mat2, T *out_mat,int mat1_rows, int mat1_cols,int mat2_rows, int mat2_cols) {// Initialize shared memoryint TILE_WIDTH = blockDim.x;extern __shared__ uint8_t shared_mem[];T *ds_mat1 = reinterpret_cast<T*>(shared_mem);T *ds_mat2 = reinterpret_cast<T*>(shared_mem + TILE_WIDTH * TILE_WIDTH * sizeof(T));int bx = blockIdx.x,  by = blockIdx.y;int tx = threadIdx.x, ty = threadIdx.y;int row = by * TILE_WIDTH + ty;int col = bx * TILE_WIDTH + tx;T out_value = 0;// Loop over the in_mat1 and in_mat2 tiles required to compute the out_mat elementfor (int ph = 0; ph < (mat1_cols + TILE_WIDTH - 1) / TILE_WIDTH; ++ph) {// Collaborative loading of in_mat1 and in_mat2 tiles into shared memoryds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows && ph * TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows && col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;// Synchronize to make sure the tiles are loaded__syncthreads();// Compute the out_mat elementfor (int k = 0; k < TILE_WIDTH; ++k)out_value += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];// Synchronize to make sure the out_mat element is computed// before other threads load new tiles__syncthreads();}// Store the out_mat element in out_matif (row < mat1_rows && col < mat2_cols)out_mat[row * mat2_cols + col] = out_value;
}

在這種方法中,我們使用共享內存來存儲輸入矩陣的塊。線程首先計算它們的行和列索引。如果行或列索引大于輸入矩陣的行數或列數,則線程返回。

int row = by * TILE_WIDTH + ty;
int col = bx * TILE_WIDTH + tx;if (row >= mat1_rows || col >= mat2_cols) return;

在每個階段:

線程將tiles加載到共享內存中。 這些tiles以合并訪存的方式加載。

// Collaborative loading of in_mat1 and in_mat2 tiles into shared memory
ds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows && ph * TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows && col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;// Synchronize to make sure the tiles are loaded
__syncthreads();

當線程加載完tile后,它們會計算輸出矩陣元素。線程通過將tiles的相應行和列相乘,并將結果相加來計算輸出矩陣元素。

// Compute the out_mat element
for (int k = 0; k < TILE_WIDTH; ++k)out_value += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];// Synchronize to make sure the out_mat element is computed
// before other threads load new tiles
__syncthreads();

最后,線程將輸出矩陣元素存儲在輸出矩陣中。

// Store the out_mat element in out_mat
out_mat[row * mat2_cols + col] = out_value;

長方形tiles

template<typename T> __global__
void mat_mat_mul_rec_tiles(T* in_mat1, T* in_mat2, T* out_mat,int mat1_rows, int mat1_cols,int mat2_rows, int mat2_cols) {// Initialize shared memoryint TILE_WIDTH = blockDim.x;extern __shared__ uint8_t shared_mem[];T* ds_mat1 = reinterpret_cast<T*>(shared_mem);T* ds_mat2 = reinterpret_cast<T*>(shared_mem + TILE_WIDTH * TILE_WIDTH * sizeof(T));T* ds_mat3 = reinterpret_cast<T*>(shared_mem + 2 * TILE_WIDTH * TILE_WIDTH * sizeof(T));int bx = blockIdx.x, by = blockIdx.y;int tx = threadIdx.x, ty = threadIdx.y;int row = by * TILE_WIDTH + ty;int col = bx * TILE_WIDTH * 2 + tx;T out_value1 = 0;T out_value2 = 0;// Loop over the in_mat1 and in_mat2 tiles required to compute the out_mat elementfor (int ph = 0; ph < (mat1_cols + TILE_WIDTH - 1) / TILE_WIDTH; ++ph) {// Collaborative loading of in_mat1 and in_mat2 tiles into shared memoryds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows&& ph* TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;ds_mat3[ty * TILE_WIDTH + TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& TILE_WIDTH + col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + TILE_WIDTH + col] : 0;// Synchronize to make sure the tiles are loaded__syncthreads();// Compute the out_mat elementfor (int k = 0; k < TILE_WIDTH; k++) {out_value1 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];out_value2 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat3[k * TILE_WIDTH + TILE_WIDTH + tx];}// Synchronize to make sure the Pvalues are computed// before other threads load new tiles__syncthreads();}// Store the Pvalues in out_matif (row < mat1_rows && col < mat2_cols)//out_mat[row][col];out_mat[row * mat2_cols + col] = out_value1;if (row < mat1_rows && TILE_WIDTH + col < mat2_cols)//out_mat[row][TILE_WIDTH + col];out_mat[row * mat2_cols + TILE_WIDTH + col] = out_value2;
}

這個kernel函數幾乎與正方形tile函數相同。

首先,線程計算出他們將計算的輸出矩陣元素的行和列。

int row = by * TILE_WIDTH     + ty;
int col = bx * TILE_WIDTH * 2 + tx;

然后線程將tile加載到共享內存中。唯一的區別是N加載2個tile。

// Collaborative loading of in_mat1 and in_mat2 tiles into shared memoryds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows&& ph* TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;ds_mat3[ty * TILE_WIDTH + TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& TILE_WIDTH + col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + TILE_WIDTH + col] : 0;

然后線程計算兩個輸出矩陣元素。

// Compute the out_mat elementfor (int k = 0; k < TILE_WIDTH; k++) {out_value1 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];out_value2 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat3[k * TILE_WIDTH + TILE_WIDTH + tx];}

最后,存儲兩個輸出矩陣元素。

// Store the Pvalues in out_mat
if (row < mat1_rows && col < mat2_cols)//out_mat[row][col];out_mat[row * mat2_cols + col] = out_value1;if (row < mat1_rows && TILE_WIDTH + col < mat2_cols)//out_mat[row][TILE_WIDTH + col];out_mat[row * mat2_cols + TILE_WIDTH + col] = out_value2;

性能分析

運行時間:

矩陣維度:1024 × 1024

線程塊維度:32 × 32

可見使用共享內存可以有效降低運行速度。但長方形tile反而耗時更久。因為L1緩存與共享內存公用硬件空間。可能共享內存占據大部分空間,而L1緩存所剩無幾,從而導致長方形tile耗時更久。具體情況需要做性能分析,之后再補充。也許是長方形tile的復雜性增加。kernel引入了許多分支和同步點。這可能導致耗時更久。

筆者采用設備:RTX3060 6GB

PMPP項目提供的分析

kernel的性能是使用NvBench項目在多個gpu中測量的。研究的性能測量方法有:

內存帶寬:每秒傳輸的數據量。

內存帶寬利用率:占用內存帶寬的百分比。

基礎方法

正方形tile

長方形tile

參考文獻:

1、大規模并行處理器編程實戰(第2版)

2、PPMP

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/41468.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/41468.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/41468.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux上web服務器搭建(Apache、Nginx)

第五章 web服務器 第一節 DNS&#xff1a;對域名進行解析&#xff0c;查詢對應的地址 1.1 web服務器簡介 www是world wide web的縮寫&#xff0c;也就是全球信息廣播的意思 1.2.網址及HTTP簡介 web服務器提供的這些數據大部分都是文件&#xff0c;那么我們需要在服務器端…

傳統視覺Transformer的替代者:交叉注意力Transformer(CAT)

傳統視覺Transformer的替代者:交叉注意力Transformer(CAT) 在深度學習的世界里,Transformer架構以其在自然語言處理(NLP)領域的卓越表現而聞名。然而,當它進入計算機視覺(CV)領域時,卻面臨著計算成本高昂和推理速度慢的雙重挑戰。現在,一項革命性的創新——交叉注意…

Qualcomm QCS6490 開發板運行高通AI Hub圖像分類程序

相關代碼可以在如下鏈接下載&#xff1a; ai-hub-models/apps/android/ImageClassification at main quic/ai-hub-models GitHub 所用硬件有&#xff1a; 1. UBUNTU20.04 2. 高通QCS6490 開發板 對下載下來的代碼進行編譯 1. ubuntu環境配置 1. python環境配置 如果你…

[SAP ABAP] 子例程

子例程 示例1 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 輸出結果如下所示 示例2 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 輸出結果如下所示 補充擴展練習 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 輸出結果如下所示 提示…

驗證圖像傳感器性能

文章目錄 驗證圖像傳感器性能 驗證圖像傳感器性能 測試類別測試項目具體方法與描述圖像質量測試分辨率測試使用分辨率測試卡&#xff08;如1951 USAF分辨率測試卡&#xff09;拍攝圖像&#xff0c;分析成像的清晰度。動態范圍測試測試傳感器在高對比度場景中的表現&#xff0c…

odoo 物聯網 設備數據采集方案

圖一 架構手稿(許老師專屬) 圖二 架構簡圖 部署 方案一&#xff1a; odoo業務數據庫與設備采集數據庫使用一個instance。 缺點&#xff1a;重啟pg服務相互影響。 方案二&#xff1a; odoo業務數據庫與設備采集數據庫獨立部署&#xff0c;使用兩個instance。 優點&#xff1a;…

RedHat / CentOS安裝FTP服務

本章教程,記錄在RedHat / CentOS中安裝FTP的具體步驟。FTP默認端口:21 1、安裝 epel 源 yum install -y epel-release2、安裝 pure-ftpd yum -y install pure-ftpd3、修改默認配置 # 默認配置位于 /etc/pure-ftpd/pure-ftpd.conf,在配置文件中找到下面幾個參數進行修改:#…

AI視頻生成技術爆發 引領虛擬數字人產業新潮流

2024年剛開局&#xff0c;先有OpenAI的AI視頻生成模型Sora驚艷全網&#xff0c;隨后阿里巴巴發布EMO&#xff0c;一張照片音頻&#xff0c;就能生成具有生動表情和各種頭部姿勢、口型完全匹配高保真的人聲頭像動態視頻。 技術的革新不僅為內容創作者打開了新世界的大門&#xf…

數據結構——隊列練習題

在C語言中&#xff0c;.和->運算符用于訪問結構體的成員變量。它們之間的區別在于&#xff1a;.運算符用于訪問結構體變量的成員。->運算符用于訪問結構體指針變量的成員 1a&#xff08;rear指向隊尾元素后一位&#xff0c;判空判滿時犧牲一個存儲單元&#xff09; 首先…

小抄 20240703

1 “這么多年&#xff0c;什么都沒有變化。” 同樣看到這句話&#xff0c;有人會覺得幸福&#xff0c;有人會覺得悲傷。 好的事沒變&#xff0c;就覺得幸福。 壞的事沒變&#xff0c;會覺得悲傷。 2 人類預測不到的大趨勢&#xff0c;只有技術大爆炸&#xff0c;關于人的那…

PEFT - 安裝及簡單使用

LLM、AIGC、RAG 開發交流裙&#xff1a;377891973 文章目錄 一、關于 PEFT二、安裝1、使用 PyPI 安裝2、使用源碼安裝 三、快速開始1、訓練2、保存模型3、推理4、后續步驟 本文翻譯整理自&#xff1a;https://huggingface.co/docs/peft/index 一、關于 PEFT &#x1f917;PEFT…

算力共享解決方案

目錄 算力共享解決方案 一、引言 二、目標 三、技術架構 一、基礎設施層 二、服務層 三、應用層 四、實施步驟 五、安全與隱私保護 六、經濟模型(信用評估-博弈論) 算力共享解決方案 一、引言 背景分析&#xff1a; 隨著大數據、人工智能、區塊鏈等技術的飛速發展&…

BugKu-WEB-sodirty

目錄 前言 正文 信息收集 代碼審計 驗證 結尾 前言 七月始,暑假副本也正式開啟 正文 信息收集 看著貌似沒啥意義 看樣子是有備份文件 下載下來 快速審計一下 代碼審計 來吧 app.js沒啥東西,主要是功能是實現error 我們找一找有沒有index.js 找到了 \www\routes\in…

MySQL的Docker部署方式

說明:Docker部署MySQL主要是簡單快速&#xff0c;不會對電腦系統造成污染。假如你的本地沒有Docker&#xff0c;或者你不會使用Docker&#xff0c;則使用PyCharm去啟動MySQL&#xff0c;或者直接在本機安裝MySQL都是可以的。最重要的是&#xff0c;你要有一個MySQL環境&#xf…

使用 Git Hooks 防止敏感信息泄露

歡迎關注公眾號&#xff1a;冬瓜白 在日常開發中&#xff0c;我們可能會不小心將敏感信息提交到 Git。為了防止這種情況&#xff0c;可以利用 Git Hooks 編寫一個簡單的腳本&#xff0c;當發現提交中包含敏感詞時&#xff0c;給出提示。 以下是一個基于 pre-commit 鉤子的示例…

踩坑:Unity導出WebGL發布到手機上豎屏時強制顯示橫屏

具體的適配問題 公司的項目需要將游戲導出WebGL 發布到Web平臺 本以為是個很簡單的事情 誰知道卻被個橫豎屏適配搞的頭暈 畢竟只有大學淺淺的學了下HTML這門語言 出來工作后基本上都是在跟C# Lua打交道 言歸正傳 看看具體問題吧 游戲如果從橫屏進入 基本上不會有什么適配問題…

C++ 多進程多線程間通信

目錄 一、進程間通信 1、管道&#xff08;Pipe&#xff09; 2、消息隊列&#xff08;Message Queue&#xff09; 3、共享內存&#xff08;Shared Memory&#xff09; 4、信號量&#xff08;Semaphore&#xff09; 5、套接字&#xff08;Socket&#xff09; 6、信號&…

Finding Global Homophily in Graph Neural Networks When Meeting Heterophily

本文發表于:ICML22 推薦指數: #paper/??? 問題背景: 異配圖的鄰接矩陣難以確定,以及異配圖的計算復雜度開銷大 可行的解決辦法:高通濾波多跳鄰居,GPRGNN(pagerank一類&#xff0c;各階鄰居的權重不同,ACM-GCN&#xff08;高低通濾波,H2GCN&#xff08;應該復雜度很大&…

碳課堂|搞清楚碳足跡,只看這篇文章就夠了

碳足跡管理是碳達峰碳中和的重要政策工具&#xff0c;2023年12月&#xff0c;國家發展改革委、工信部、國家市場監管總局、住房城鄉建設部、交通運輸部等部門聯合印發《關于加快建立產品碳足跡管理體系的意見》&#xff0c;對產品碳足跡管理各項重點任務作出系統部署。 推動碳…

音樂播放器

目錄 一、設計目標二、實現流程1. 數據庫操作2. 后端功能實現3. 前端UI界面實現4. 程序入口 三、項目收獲 一、設計目標 1. 模擬網易云音樂&#xff0c;實現本地音樂盒。 2. 功能分析&#xff1a; 登錄功能窗口顯示加載本地音樂建立播放列表播放音樂刪除播放列表音樂 3.設計思…