中文字幕国产91无码|AV成人手机在线|av成人先锋在线|911无码在线国产人人操|91蜜桃视频精品免费在线|极品美女A∨片在线看|日韩在线成人视频日韩|电影三级成人黄免费影片|超碰97国产在线|国产成人精品色情免费视频

  • +1

FFmpeg AI 推理+圖形渲染的可定制 GPU 管線

2022-12-12 17:06
來源:澎湃新聞·澎湃號(hào)·湃客
聽全文
字號(hào)

編者按:FFmpeg作為業(yè)界廣泛使用的轉(zhuǎn)碼平臺(tái),提供了豐富高效的視頻處理能力。LiveVideoStackCon2022上海站大會(huì)我們邀請到了英偉達(dá)GPU計(jì)算專家 王曉偉老師,結(jié)合具體項(xiàng)目實(shí)踐為大家詳細(xì)介紹如何在FFmpeg中開發(fā)一個(gè)包含AI推理+圖形的完整GPU轉(zhuǎn)碼管線。

文/王曉偉

整理/LiveVideoStack

大家好,首先自我介紹一下,我是王曉偉,來自英偉達(dá)GPU計(jì)算專家團(tuán)隊(duì)。我們團(tuán)隊(duì)長期支持業(yè)界頭部廠商在GPU上進(jìn)行轉(zhuǎn)碼和計(jì)算的開發(fā)及優(yōu)化,主要包括GPU的計(jì)算加速,涉及推理、計(jì)算和編解碼。本次主要跟大家分享下如何在FFmpeg中定制一個(gè)在GPU上的包含AI推理和圖形渲染的pipeline。

在正式分享之前,我們先來回顧下使用GPU轉(zhuǎn)碼的歷史進(jìn)程。

最初,在視頻行業(yè)或互聯(lián)網(wǎng)行業(yè)GPU只是用于單純的轉(zhuǎn)碼,記得在五、六年前,一些業(yè)界的頭部廠商購買了很多GPU來做轉(zhuǎn)碼。如圖所示,NVENC和NVDEC是GPU的硬件,用于解碼和編碼的芯片,硬件編解碼的好處有成本低、吞吐高和延遲低。但現(xiàn)在GPU越做越大,安培架構(gòu)的GPU有幾百平方毫米的核心,NVENC和NVDEC這兩個(gè)硬件的編解碼芯片只占用了GPU核心很小的部分。這時(shí)單獨(dú)買一塊價(jià)格昂貴的卡僅用于做轉(zhuǎn)碼很不劃算。

另一方面,業(yè)務(wù)內(nèi)容、技術(shù)和算法在變化,此時(shí)我們所熟知的轉(zhuǎn)碼不再是指轉(zhuǎn)碼這一個(gè)單一行為(也就是不只是把A格式轉(zhuǎn)到B格式,或者把A的碼流轉(zhuǎn)到某些分發(fā)的格式),而是加入了很多的處理,比如推理、畫質(zhì)的提升或計(jì)算。

視頻是由連續(xù)的圖片組成的,對視頻做計(jì)算就是對連續(xù)的圖片做計(jì)算。如果圖片的分辨率較高,如720p、1080p、4K和8K等,那么對算力的要求就會(huì)很高,而GPU作為高吞吐、高帶寬、高算力的硬件,可以更為方便地處理視覺和圖像任務(wù)。最近兩年,大家越來越多地使用GPU來做轉(zhuǎn)碼和計(jì)算,這里就有一個(gè)經(jīng)驗(yàn)或趨勢,稱之為全流程GPU,即數(shù)據(jù)駐留在GPU上,不會(huì)在CPU和GPU間來回地拷貝。PCIe的帶寬和GPU顯存的帶寬有數(shù)量級的差異,即PCIe的帶寬不高,來回地拷貝會(huì)造成額外的延遲甚至額外的吞吐上的損失,因此建議數(shù)據(jù)駐留GPU,避免來回地拷貝,使得所有的計(jì)算盡量在GPU上進(jìn)行。

回顧完歷史,接下來介紹我們本次分享內(nèi)容的背景。

首先,我們注意到視頻云渲染在數(shù)據(jù)中心中受到越來越多的關(guān)注。云渲染這個(gè)詞聽起來很寬泛,大家可以把它和業(yè)界的一些具體應(yīng)用聯(lián)系起來,比如現(xiàn)在很火的云特效,數(shù)字人和虛擬主播等等,這些都是很多廠商努力開拓的新領(lǐng)域。云渲染涉及的技術(shù)棧較為復(fù)雜,它包括AI推理、圖形、圖形渲染、計(jì)算和轉(zhuǎn)碼等,雖然GPU可以實(shí)現(xiàn)這些內(nèi)容,但難點(diǎn)是如何將這些內(nèi)容有機(jī)地結(jié)合起來。這時(shí),一條業(yè)務(wù)線可能就會(huì)很復(fù)雜,比如做一個(gè)數(shù)字人或者虛擬主播的業(yè)務(wù),既需要推理和渲染,又需要轉(zhuǎn)碼,同時(shí)對性能、延時(shí)有很高要求,因此我們需要考慮如何把這些技術(shù)合理地組織起來。

我們在和部分業(yè)界的頭部客戶交流時(shí)了解到,他們通常是會(huì)通過十幾個(gè)部門間的合作來實(shí)現(xiàn)云渲染的流水線。其中包含做轉(zhuǎn)碼的部門、做渲染的部門、做AI算法的部門,以及做CV的部門等等,開會(huì)時(shí)就需要十幾個(gè)部門同時(shí)參加會(huì)議,因此溝通的成本非常巨大。同時(shí),他們也缺少參考實(shí)現(xiàn),是在靠自己摸索實(shí)現(xiàn),這個(gè)過程中就會(huì)遇到很多問題。

因此,我們想將之前的一些經(jīng)驗(yàn)總結(jié)出來,跟大家分享一下我們所遇到過的“坑”,這樣就可以為大家提供一個(gè)類似的參考實(shí)現(xiàn)。這個(gè)參考實(shí)現(xiàn)可能看起來非常簡單,但它提供的是一個(gè)清晰的流程,然后再從簡單到復(fù)雜,一步步地將其開發(fā)出來。即使這個(gè)參考實(shí)現(xiàn)無法支撐大家直接做出一個(gè)產(chǎn)品,但起碼可以做一個(gè)demo出來,并且可以在內(nèi)部進(jìn)行測試和評估。

目前,這項(xiàng)工作已經(jīng)開源,倉庫地址如圖中所示,名稱為“FFmpeg-GPU-Demo”,這是一個(gè)針對GPU修改過的FFmpeg的實(shí)現(xiàn),后期我們會(huì)繼續(xù)迭代這個(gè)倉庫,會(huì)有大的版本升級,大家可以關(guān)注下。

介紹完整體背景,接下來看一下我們對場景的假設(shè)。

因?yàn)橐鶕?jù)具體的場景來做流水線,假設(shè)要做的是虛擬主播、數(shù)字人,那么我們需要哪些組件呢?首先,我們需要面部的追蹤和建模,因?yàn)橐槍θ四樳M(jìn)行渲染,但由于數(shù)字人不是真人,就要把人臉的表情和動(dòng)作retarget過去;然后,還需要特效渲染、視頻增強(qiáng)(比如超分、去噪等),需要用綠幕進(jìn)行摳圖、視頻轉(zhuǎn)碼等等。每一個(gè)組件需要由不同的團(tuán)隊(duì)負(fù)責(zé),每個(gè)團(tuán)隊(duì)有自己所屬的專業(yè)領(lǐng)域,可能對其他方向的內(nèi)容了解得并不多。

此外,大家所使用的底層的技術(shù)也不一樣,例如轉(zhuǎn)碼團(tuán)隊(duì)希望繼續(xù)使用FFmpeg,因?yàn)樗麄兛赡軐Fmpeg更熟悉。而特效團(tuán)隊(duì)有自己的渲染器(自己寫的OpenGL Shader或商用的UE、Unity),AI算法團(tuán)隊(duì)(可能不會(huì)C語言或C++)直接使用PyTorch中訓(xùn)練好的模型。以上這些都是需要考慮到的問題。

帶著這些問題,我們來介紹一下本次分享的具體內(nèi)容。首先,會(huì)跟大家介紹下我們的pipeline是如何設(shè)計(jì)的。之后會(huì)詳細(xì)講解為了做這個(gè)pipeline或設(shè)計(jì),需要做哪些工作和步驟,有哪些需要注意的點(diǎn)。最后,會(huì)介紹我們現(xiàn)有的一些工作以及性能分析,探討我們所看到的行業(yè)內(nèi)部的發(fā)展趨勢并對云渲染的未來進(jìn)行展望。

01 FFmpeg推理+渲染管線

首先,介紹下我們的pipeline是如何設(shè)計(jì)的。

首先,我們要確定具體的目標(biāo)場景,即要做什么。我們想找一個(gè)典型的,不太復(fù)雜和龐大的場景,因此我們這里選定的場景是人臉渲染。人臉渲染是數(shù)字人、虛擬主播的一個(gè)子集。

如圖為大家展示下我們的pipeline用FFmpeg實(shí)際跑出來的效果,其中涉及兩個(gè)關(guān)鍵點(diǎn):由于要將口罩畫在人臉上,首先要對視頻中的人臉做實(shí)時(shí)的姿態(tài)估計(jì),因此采用了深度學(xué)習(xí)的模型;然后要采用超分改善圖像的性能,我們希望只在720P上做推理(這樣可以減小對算力的需求),并將圖像超分到2K,保證圖像的質(zhì)量。

這里要說明的是該項(xiàng)目并不是一個(gè)開箱即用的產(chǎn)品,我們并不追求漂亮的成品效果,只是希望借此項(xiàng)目向大家展示如何定制一個(gè)類似的管線,分享開發(fā)的經(jīng)驗(yàn)。

如之前所說,我們希望遵守全GPU流程的準(zhǔn)則,避免PCIe數(shù)據(jù)的拷貝,將計(jì)算和數(shù)據(jù)都留在GPU上,避免拷貝帶來的開銷。因此,我們可以確定兩點(diǎn):首先,要基于FFmpeg進(jìn)行開發(fā);其次,要使用GPU進(jìn)行編解碼,這樣能保證延遲和吞吐。

為了實(shí)現(xiàn)剛剛給大家展示的效果,要使用兩個(gè)深度學(xué)習(xí)的模型。首先要做Face Alignment,即對面部的姿態(tài)進(jìn)行估計(jì),將得到的結(jié)果在OpenGL里進(jìn)行繪制。另一個(gè)深度學(xué)習(xí)模型是超分,超分的效果類似DLSS,后面會(huì)進(jìn)行詳細(xì)地介紹。

這是管線設(shè)計(jì)的第二部分,就是從垂直的角度來看它涉及到哪些棧。

首先,介紹一下為什么要用FFmpeg,原因是“無他,唯用的人多爾”,我們了解到的客戶大多都使用FFmpeg,尤其在直播和短視頻領(lǐng)域,大家很多都是基于FFmpeg進(jìn)行轉(zhuǎn)碼。大家之所以都用FFmpeg,是因?yàn)镕Fmpeg比較“親民”,比較簡單,沒有太多復(fù)雜的機(jī)制。但簡單的同時(shí)也帶來一些限制,這個(gè)后面會(huì)再進(jìn)行說明。同時(shí),這也說明了“技術(shù)之間沒有好壞,只有合不合適”。

大家對FFmpeg了解得比較多,垂直來看若要做一條鏈路,底層是硬件,GPU做了“重活”。然后中間有一些軟件,底層的軟件有CUDA、OpenGL和NVIDIA的Codec SDK(硬件編解碼),上層的軟件有Pytorch、ONNX和TensorRT(推理),還有其它的一些未列出來的軟件。

FFmpeg使用avfilter來處理解碼后的幀,做全流程的GPU處理實(shí)際就是要實(shí)現(xiàn)若干FFmpeg GPU filter。我們要調(diào)用中間軟件的能力在libav層做開發(fā),最后在ffmpeg binary調(diào)用libav,比如使用ffmpeg的命令行直接調(diào)用做好的filter進(jìn)行轉(zhuǎn)碼。理想情況,做兩個(gè)filter就夠了,TensorRT filter用于推理,OpenGL filter用于渲染,硬件編解碼是現(xiàn)成的。

但實(shí)際上這樣是不行的,為什么呢?Gstreamer的能力很強(qiáng),全模塊化的設(shè)計(jì)給它帶來了非常靈活的優(yōu)勢,但問題是FFmpeg設(shè)計(jì)時(shí)只為視頻設(shè)計(jì),沒有考慮過在各種element之間傳遞非圖像數(shù)據(jù)。推理一般產(chǎn)生的數(shù)據(jù)是tensor數(shù)據(jù),它可能是高維的,有不同的精度(int8, half, FP32)。FFmpeg 的filter間傳送的是AVFrame,但很難將tensor數(shù)據(jù)放到AVFrame中,比如維度信息就很難裝入其中。如果想要傳遞非圖像數(shù)據(jù),業(yè)界中有些選擇直接對libav進(jìn)行底層修改,添加一條新的data path,即在此之間可以傳遞任意格式的數(shù)據(jù)。但這個(gè)工作非常底層,很多業(yè)界大廠、頭部客戶不愿意做這個(gè)工作,因?yàn)榧词共豢紤]開發(fā)的工程量,底層的修改涉及到整個(gè)FFmpeg,牽扯面太大,要做大量的測試保證線上的穩(wěn)定性。并且在修改時(shí),可能會(huì)出現(xiàn)在A處做了更改,結(jié)果B處發(fā)生了問題的現(xiàn)象,就如量子力學(xué)一樣,某個(gè)地方的變化影響了其他地方的改變。因此,很多人并不愿意進(jìn)行底層的修改。

對此,我們想了一個(gè)簡單點(diǎn)的方法。不傳遞非圖像數(shù)據(jù),在既有渲染又有推理的場景下,若渲染和推理是緊密結(jié)合的,就將這兩者放到同一個(gè)filter中。在一個(gè)filter中的處理就比較方便了,推理出來的數(shù)據(jù)通過互操作直接傳給OpenGL,不經(jīng)過CPU而是直接在GPU上交換數(shù)據(jù),然后在一個(gè)filter中完成操作后,OpenGL將所需繪畫的內(nèi)容畫好并直接將內(nèi)容傳給后續(xù)的filter,這樣輸入和輸出的都是圖像數(shù)據(jù),就不會(huì)有之前的問題。

但相對來說,這種情況下的渲染的filter就會(huì)比較復(fù)雜,如圖中的結(jié)構(gòu)所示,進(jìn)入以后要先做渲染相關(guān)的推理,推理的結(jié)果要通過互操作傳給OpenGL做渲染,然后再輸出幀,再進(jìn)行后面的操作,后面可以接其他的GPU的filter或者TRT的推理。

接下來具體介紹一下Rendering filter。我們選用了兩個(gè)不同模型進(jìn)行面部姿態(tài)估計(jì),這兩個(gè)模型不太一樣,各自具有代表性。

兩個(gè)模型分別是img2pose和3DDFA v2,這兩個(gè)都是開源的項(xiàng)目,開源的倉庫地址已經(jīng)給出,大家有興趣的話可以去了解一下。之前看到的演示視頻里的內(nèi)容是用img2pose生成的,它的模型是Faster R-CNN類型的模型,這適合大規(guī)模的人臉檢測,若會(huì)場里有50個(gè)人,這個(gè)模型檢測一個(gè)人的時(shí)間和檢測50個(gè)人的時(shí)間是沒有區(qū)別的,后面會(huì)給大家展示具體的性能數(shù)據(jù)。

但由于img2pose模型是Faster R-CNN類型的模型,對算力的要求就很高。3DDFA v2是一個(gè)輕量模型,它的結(jié)果為3DMM參數(shù),根據(jù)這個(gè)參數(shù)可以重建出人臉的模型,這個(gè)模型是一個(gè)Mesh,由于模型較為輕量,因此推理更快。但該模型不適合于大規(guī)模的人臉檢測,最適用的場景是只有一兩張人臉,當(dāng)人數(shù)過多時(shí),性能就會(huì)線性下降。渲染filter的結(jié)構(gòu)如圖所示,將經(jīng)過Face alignment處理后的數(shù)據(jù)傳給OpenGL,然后渲染輸出。

除了渲染模型,還有一個(gè)超分模型,這個(gè)模型是用TensorRT filter去實(shí)現(xiàn)的。TensorRT filter的實(shí)現(xiàn)沒有太多麻煩的地方,可直接根據(jù)TensorRT的例子開發(fā)一個(gè)filter,然后將其包起來放進(jìn)去就可以了。其中有一點(diǎn)要注意的是,TensorRT對數(shù)據(jù)格式有要求,它只支持NCHW數(shù)據(jù),不支持NHWC數(shù)據(jù),映射到視頻圖片方面就是,它只支持planar RGB,不支持packed RGB。但在FFmpeg中只能看到packed RGB,排序方式是“RGB RGB RGB...”,而不是“RRR...GGG...BBB...”,因此FFmpeg中沒有這種格式,而且FFmpeg也不支持float32,即沒有planar RGB float32格式。

針對上述情況,我們在FFmpeg中添加了一種新的pixel format,稱為rgbpf32(planar RGB float32),并且實(shí)現(xiàn)了一個(gè)新的forma_cuda filter,就是用cuda寫了一個(gè)kernel,來支持在GPU上nv12和rgbpf32之間的轉(zhuǎn)換。在FFmpeg中調(diào)用上述內(nèi)容的命令如圖中所示,前面部分是為了保證能使用GPU的編解碼并使得數(shù)據(jù)駐留在GPU上,然后是輸入文件的命令,接著是使用GPU上scale filter的命令,使用format_cuda轉(zhuǎn)成rgbpf32的命令,使用TensorRT模型的命令,再是轉(zhuǎn)回nv12的命令,最后是nvenc編碼輸出的命令。圖中展示了上述命令的流程。

02 定制FFmpeg GPU Filter

介紹完整個(gè)pipeline的設(shè)計(jì)后,接下來講解一些具體的技術(shù),即如何在FFmpeg中定制一個(gè)GPU Filter。這方面的資料其實(shí)比較少,文檔中可以看到如何去寫一個(gè)filter,但大部分是講解怎么做CPU上的filter,這與GPU上的filter不太一樣,即與涉及異構(gòu)硬件加速的filter不太一樣。對此,我們總結(jié)了一些經(jīng)驗(yàn)。

首先是GPU的顯存管理。FFmpeg中的filter大概可分為幾步,如圖中所示(這里只展示簡單的路徑,還有其他的更高級復(fù)雜的路徑),先是init,然后是query_formats(協(xié)商格式),接著是初始化,中間進(jìn)行一些信息的配置,比如根據(jù)輸入輸出大小分配memory,然后是filter_frame,這是filter邏輯實(shí)際發(fā)生的地方,每來一幀就會(huì)調(diào)用filter_frame來處理圖片,并實(shí)現(xiàn)輸入輸出,最后釋放資源。

通常來說,做CPU開發(fā)時(shí),會(huì)在init()中分配filter需要的memory。但是在init()中,F(xiàn)Fmpeg并沒有完成GPU的初始化,只有當(dāng)創(chuàng)建CUDA context后才能完成FFmpeg的初始化,因?yàn)槿魏我粋€(gè)GPU程序都需要一個(gè)CUDA context。創(chuàng)建過程可能是透明的或者顯式的,即不一定要手動(dòng)創(chuàng)建,比如使用CUDA Runtime API時(shí),我們不需要管CUDA context,因?yàn)镽untime會(huì)自動(dòng)維護(hù)CUDA context。那CUDA context到底是什么呢?可以把它和CPU上進(jìn)程的上下文做類比,GPU顯存的地址空間和設(shè)備等信息都保存在CUDA context中。

總之,完成GPU的初始化就是要?jiǎng)?chuàng)建CUDA context。那么在init()中,由于沒有創(chuàng)建CUDA context,就沒有完成GPU的初始化,因此不能對GPU進(jìn)行操作,不能分配和拷貝顯存,且發(fā)起API調(diào)用時(shí)會(huì)報(bào)錯(cuò)。那CUDA context什么時(shí)候才能被創(chuàng)建呢?在協(xié)商的第二步,即config_props()中CUDA context會(huì)被創(chuàng)建,然后就可進(jìn)行顯存的分配、拷貝,而且在config_props()中可以知道輸入輸出的大小,因此可以分配一個(gè)緩存buffer,其與幀的大小有關(guān)。

在filter_frame()中,我們必須手動(dòng)分配輸出幀,然后釋放當(dāng)前filter中的輸入幀,即filter_frame()既要負(fù)責(zé)釋放輸入,又要負(fù)責(zé)分配輸出。這意味著,每來一幀就要分配一次memory,free一次memory。在GPU上頻繁地malloc和free顯存是非常昂貴的,因?yàn)槊吭贕PU上做一次memory分配,就要做一次GPU全局的同步,這會(huì)帶來性能上的損失。大家要注意的是,此處需要使用libavutil中提供的分配接口(大家可以去我們的倉庫代碼里看具體的接口,里面有具體的示例展示如何對其進(jìn)行使用),因?yàn)閘ibavutil中為幀的分配實(shí)現(xiàn)了一個(gè)顯存池。FFmpeg中有buffer pool,GPU中也實(shí)現(xiàn)了buffer pool,在初始化GPU時(shí),會(huì)預(yù)先分配一大塊顯存,之后再需要顯存時(shí)直接從顯存池里獲取,而不是去調(diào)用malloc。直接從顯存池里獲取顯存是一個(gè)簡單的操作,復(fù)雜度為O(1),這樣就不必?fù)?dān)心頻繁的malloc降低GPU性能。

大家可能對剛才提到的CUDA context還有疑問,我再給大家解釋一下。之前提到,不是所有的情況下都需要手動(dòng)管理CUDA context,那怎么來區(qū)分這個(gè)情況呢?是看使用的哪種CUDA API。若使用的是CUDA Driver API,這個(gè)API更底層,若使用它則需要手動(dòng)管理CUDA context;若使用的是CUDA Runtime API,這個(gè)API更高層,若使用它則不需要手動(dòng)管理CUDA context。手動(dòng)管理CUDA context不是一件特別有意義的事情,所以我們一般建議大家使用CUDA Runtime API,讓其管理CUDA context,這既不會(huì)出錯(cuò)也不會(huì)影響性能。

目前,多數(shù)庫會(huì)使用CUDA Runtime API,比如TensorRT。但FFmpeg使用的是Driver API,這是無法改變的,我們只能遵守其規(guī)則。我們建議大家在開發(fā)filter或自己寫filter代碼時(shí),使用Runtime API,因?yàn)镽untime API和Driver API是可以共存的。Runtime API使用起來更方便,比如做CUDA memory copy時(shí),Driver API更“啰嗦”一些,Runtime API相對更簡潔。因此我們推薦使用Runtime API,但使用其的前提是管理好CUDA context。也就是說,F(xiàn)Fmpeg創(chuàng)建一個(gè)CUDA context,Runtime啟動(dòng)時(shí)會(huì)識(shí)別當(dāng)前線程是否有可用的CUDA context,若有則直接使用當(dāng)前的CUDA context,若沒有則先創(chuàng)建一個(gè)CUDA context。因此需要注意的是,一定要將FFmpeg創(chuàng)建的CUDA context設(shè)置為當(dāng)前線程可用的context,這樣CUDA Runtime不會(huì)創(chuàng)建新的context而是直接使用FFmpeg創(chuàng)建的CUDA context。設(shè)置的過程就是一個(gè)入棧的過程,接口叫做cuCtxPushCurrent,可將當(dāng)前的CUDA context給push到線程中,即入棧,然后就可以使用了。

這里給大家一個(gè)建議,在任何CUDA調(diào)用前先將CUDA context入棧,調(diào)用結(jié)束后立即出棧,這樣就是一個(gè)干凈的CUDA context管理,不容易發(fā)生錯(cuò)誤。若CUDA context出錯(cuò),就不能訪問memory,因?yàn)槭褂肍Fmpeg的硬件解碼器得到的幀將存在GPU的顯存里,這個(gè)顯存是在FFmpeg分配的CUDA context下獲取的,而CUDA有一個(gè)規(guī)定,即在B context下不能訪問在A context下分配的memory,這時(shí)候強(qiáng)行訪問就會(huì)報(bào)錯(cuò),因此若Runtime創(chuàng)建了一個(gè)CUDA context,在該context下就不能再訪問解碼得到的幀。因此一定要管理好CUDA context。

另外,如果大家遇到圖中展示的錯(cuò)誤,比如invalid resource handle、invalid memory access和cudnn status mapping error等,可以檢查CUDA context是否發(fā)生改變。CUDA有接口可以打印當(dāng)前的CUDA context內(nèi)容,大家可以獲取該內(nèi)容觀察filter運(yùn)行過程中CUDA context是否發(fā)生改變,若改變則可能出現(xiàn)問題。

最后,給大家介紹一下數(shù)據(jù)擺放。FFmpeg在GPU上支持的pixel format不太多,基本上是nv12、yuv420p和rgb0(0rgb)三種。其中除yuv420p外,nv12和rgb0的地址在GPU上均連續(xù),此處的連續(xù)指的是不同的channel,比如nv12的Y通道和UV通道是連續(xù)的,是一整個(gè)顯存,這個(gè)顯存被切分使用而已。

另一個(gè)很重要的點(diǎn)是,F(xiàn)Fmpeg會(huì)對GPU memory進(jìn)行對齊,對齊值為一個(gè)設(shè)備的屬性,名稱是CU_DEVICE_ATTRIBUTE_TEXTURE_ALIGNMENT,可以通過CUDA的接口查詢該值。這個(gè)值通常為512字節(jié),即AVFrame.linesize通常為512的倍數(shù),但實(shí)際上幀大小可能不是512的倍數(shù),這時(shí)會(huì)做padding,將其補(bǔ)齊到512的倍數(shù)。另外部分filter也會(huì)將高度對齊到32,但是實(shí)際上高度可能不是32的倍數(shù)。此時(shí)許多op或者推理引擎不支持帶有padding/對齊的輸入,比如TensorRT、PyTorch。如果將帶有padding的數(shù)據(jù)(幀的右邊和下邊帶有黑邊)輸入進(jìn)去做推理,得到的推理結(jié)果可能有問題,比如可能由于黑邊導(dǎo)致精度有問題。因此大家處理時(shí)需要小心,若推理引擎不支持對齊的輸入,要裁切掉黑邊,即做一次數(shù)據(jù)拷貝。

03 CUDA OpenGL互操作

接下來介紹CUDA 和OpenGL的互操作。

首先,介紹什么是CUDA和OpenGL的互操作。剛才提到,我們既要推理又要渲染,渲染取決于推理的結(jié)果,因此我們需要用OpenGL在GPU解碼的圖片幀上進(jìn)行繪制,那么就需要OpenGL可以訪問到CUDA memory。雖然從硬件上來看,OpenGL和CUDA memory都是用的GPU的顯存,但從軟件上來講,這二者是不相通的,存在一定的隔閡,具體原因如下:CUDA使用和C一樣的malloc/free管理機(jī)制,它使用指針來管理顯存,但OpenGL是一個(gè)基于狀態(tài)的API,使用和C語言不同的機(jī)制,它里面的內(nèi)容和數(shù)據(jù)存在buffer object當(dāng)中,buffer object的類型是有限的,這些類型是預(yù)定義好的,需要使用時(shí)就分配給某一種buffer object,而不是采用指針的方式,不能對其進(jìn)行拷貝或malloc操作,故我們無法將一塊CUDA memory直接拷貝到OpenGL的buffer object中。

面對上述情況,我們可以做的是CUDA和OpenGL的互操作,可以將OpenGL的buffer object映射到CUDA地址空間并得到對應(yīng)的指針,這個(gè)指針指向buffer object實(shí)際使用的物理顯存,故得到這個(gè)指針后二者就處于同一地址空間,此時(shí)就可以方便地實(shí)現(xiàn)拷貝。互操作有兩種實(shí)現(xiàn)路徑,一種是直接映射texture,即直接將紋理映射過去變成CUDA Array,Array與指針不同,使用起來不是很直觀。因此我們推薦另一種路徑,即使用Pixel Buffer Object將圖片進(jìn)行中轉(zhuǎn),中轉(zhuǎn)后得到的是指針,這樣使用起來就較為直觀。

給大家展示一下具體的流程。CUDA和OpenGL的互操作是從分配OpenGL的buffer開始的,圖中與OpenGL相關(guān)的內(nèi)容用橙色方框表示,與CUDA相關(guān)的內(nèi)容用綠色方框表示。如圖所示,首先要分配一個(gè)PBO(Pixel Buffer Object),共有兩種PBO,一個(gè)是pack另一個(gè)是unpack,向OpenGL里傳輸內(nèi)容時(shí)需要unpack,從OpenGL里往外傳輸內(nèi)容時(shí)需要pack。分配好后,在CUDA里注冊unpack buffer,得到graphics resources數(shù)據(jù)結(jié)構(gòu),對該數(shù)據(jù)結(jié)構(gòu)進(jìn)行map即可得到指針,然后可在我們寫的CUDA kernel里直接訪問內(nèi)容,并對NV12解碼得到的幀做一次RGBA轉(zhuǎn)換,因?yàn)镺penGL里不支持yuv數(shù)據(jù)格式,將轉(zhuǎn)換后的結(jié)果存在得到的指針里。

然后,對之前得到的resource進(jìn)行unmap操作,保證CUDA里的操作都已結(jié)束,若不進(jìn)行unmap操作而直接執(zhí)行OpenGL的操作,就會(huì)出現(xiàn)問題。執(zhí)行完unmap操作后,經(jīng)過RGBA轉(zhuǎn)換的數(shù)據(jù)已存在于unpack buffer里,使用相應(yīng)的接口將unpack buffer里的內(nèi)容讀到Texture中,就可在Texture上根據(jù)之前推理的結(jié)果進(jìn)行相應(yīng)的繪制,比如之前展示的繪制的口罩。畫完后的結(jié)果會(huì)存于Framebuffer中,然后將Framebuffer的內(nèi)容讀到CPU或者pack buffer(另外一種PBO)中,pack buffer在GPU上。若將Framebuffer的內(nèi)容讀到CPU中會(huì)有一個(gè)問題,比如我們之前有個(gè)客戶不知道互操作,他將內(nèi)容讀到CPU進(jìn)行中轉(zhuǎn),然后將內(nèi)容從CPU拷貝到CUDA的地址空間,這樣的來回中轉(zhuǎn)會(huì)導(dǎo)致延遲的增長和吞吐的下降。相反地,將內(nèi)容讀到PBO中,在GPU上做顯存的拷貝,這時(shí)的帶寬很高且速度很快。得到pack buffer后,對其進(jìn)行注冊、映射,得到指針,再用GPU上的kernel將其轉(zhuǎn)為NV12,最后得到輸出幀。

最后總結(jié)一下流程。首先在OpenGL里進(jìn)行分配,然后映射、寫入數(shù)據(jù)、創(chuàng)建texture,接著繪制,讀出framebuffer里的內(nèi)容并將其映射到CUDA地址空間中,最后將地址中的內(nèi)容寫到輸出幀中。

04 FFmpeg OpenCV GPU filter

目前已經(jīng)基本介紹完我們的pipeline涉及的點(diǎn),接下來介紹一下我們其他的工作。

其實(shí),GPU在FFmpeg上的生態(tài)不是很好,F(xiàn)Fmpeg提供的GPU filter數(shù)目有限,目前僅有5個(gè)GPU filter,其中有兩個(gè)filter的功能一樣,只不過是實(shí)現(xiàn)方式不同,這5個(gè)filter的具體功能分別是:scale_npp和scale_cuda用于縮放,yadif_cuda用于解上下場,overlay_cuda用于打水印,thumbnail_cuda用于縮略圖。不少客戶會(huì)因?yàn)樾枰牟僮鳑]有GPU實(shí)現(xiàn)而轉(zhuǎn)用CPU filter。轉(zhuǎn)用CPU filter會(huì)有一個(gè)問題,目前GPU的密度越來越高,單機(jī)四卡和單機(jī)八卡都很常見,單機(jī)八卡里可能有兩個(gè)CPU,兩個(gè)CPU大概共有八十個(gè)核,那么它既要調(diào)度八個(gè)GPU,又要運(yùn)行filter,那么吞吐可能就跟不上,并且因?yàn)閒ilter都由軟件實(shí)現(xiàn),這會(huì)帶來延遲的增加。

DevTech里有一個(gè)CV-CUDA的項(xiàng)目,里面提供了GPU加速后的常見的圖像處理op,包括OpenCV、DALI和torchvision,這使得性能得到了保障。同時(shí),項(xiàng)目還支持batch。在制作這個(gè)展示的PPT時(shí),項(xiàng)目可提供46個(gè)op,而現(xiàn)在項(xiàng)目可支持五十多個(gè)op。項(xiàng)目即將在GitHub開源(估計(jì)在本月就會(huì)上線)。圖中右半部分列出了部分op,其中有較常見的op,比如cvtColor、resize。resize的功能和scale的功能是相同的,在深度學(xué)習(xí)訓(xùn)練中會(huì)用到OpenCV里的resize,但在推理時(shí)若使用其他縮放的filter,輸出的數(shù)據(jù)可能不是比特/像素對齊的,那么和訓(xùn)練時(shí)相比,模型在線上運(yùn)行時(shí)的精度是有波動(dòng)的。而我們會(huì)做到像素對齊的結(jié)果,保證線上的精度。

我們計(jì)劃逐步將合適的OpenCV op開發(fā)為FFmpeg GPU filter,豐富GPU在FFmpeg上的生態(tài)。目前,我們正在開發(fā)vf_format_gpu。眾所周知,format filter可以在常見的pixel format間來回轉(zhuǎn)換,操作非常方便,所以我們的目標(biāo)是基于cvtColor支持在GPU上進(jìn)行pix_fmt轉(zhuǎn)換。同時(shí),F(xiàn)Fmpeg里有個(gè)組件叫l(wèi)ibswscale,這個(gè)組件非常強(qiáng)大,可以實(shí)現(xiàn)各種格式間的轉(zhuǎn)換,還可以做圖片的縮放和數(shù)據(jù)格式的轉(zhuǎn)換,甚至在FFmpeg的兩個(gè)filter大小不一致或pixel format不一致的情況下,可以自動(dòng)實(shí)現(xiàn)縮放。因此,我們在考慮是否有可能參照libswscale實(shí)現(xiàn)libgpuscale,這樣就可以在GPU上方便地使用filter,還可支持各種格式的轉(zhuǎn)變,幫助解決問題。

05 性能分析

接下來介紹一下pipeline的性能。

最關(guān)鍵的性能是異步性,尤其對于異構(gòu)計(jì)算、硬件加速來說,異步性是最重要的。異步性保證GPU可以一直處于忙碌的狀態(tài),不會(huì)有空閑時(shí)刻,讓GPU得到了充分的利用。在我們的pipeline中,有異步執(zhí)行的部分,如下所示。首先是CUDA kernel發(fā)起和執(zhí)行,其只有異步而沒有同步的方式,但可以通過調(diào)用API進(jìn)行手動(dòng)同步。然后是TRT推理,其本質(zhì)是CUDA kernel的發(fā)起和執(zhí)行,所以它也是異步的,當(dāng)然它也有同步的接口,可在CUDA kernel執(zhí)行完后進(jìn)行手動(dòng)同步。除了一些必須要同步的命令以外,OpenGL的渲染命令在GPU上基本也是異步的。GPU視頻的編碼和解碼也基本上是異步的。但同時(shí),pipeline中也有同步的部分,即OpenGL的互操作,在映射或者Unmap OpenGL graphics resources時(shí),就會(huì)同步一次。這與OpenGL的機(jī)制有關(guān),將resource map出來后,要保證在CUDA操作完成后,再unmap回去,所以要進(jìn)行同步,若不同步,直接將其unmap回去,此時(shí)若CUDA操作只完成了一半,那么傳回去的圖片只有一半??傊?,每處理一幀圖像都存在一次同步操作。

接著看一下具體的性能數(shù)據(jù),這個(gè)性能數(shù)據(jù)是從常見的推理使用的數(shù)據(jù)中心的卡上測得的。輸入視頻是720p 30幀,使用Maxine SDK的內(nèi)部超分模型,使用TRT運(yùn)行。Img2pose模型使用onnxruntime加速,由于Img2pose模型的后處理涉及矩陣的乘和一些小矩陣(4×4或8×8)的乘,所以使用了Eigen進(jìn)行重構(gòu),而對于一些后處理的op,使用了C++的torchvision進(jìn)行了手動(dòng)重構(gòu)。

我們先來分析圖中下方表格的性能數(shù)據(jù),這顯示了Img2pose模型本身的性能。將Img2pose模型分為兩個(gè)部分來看,首先是網(wǎng)絡(luò)的性能,在A10上大概是32fps,差不多是一路實(shí)時(shí)的效果;但重構(gòu)完后的后處理可以跑到5000fps以上,所以后處理占用的算力或者時(shí)間是很少的,主要的問題還是在網(wǎng)絡(luò)上。然后來看圖中上方的表格,即整體的pipeline的性能數(shù)據(jù)。將pipeline放到FFmpeg上運(yùn)行,若不跑超分模型,在A10上大概是31fps,和之前的數(shù)據(jù)很相似,說明主要的性能瓶頸就在Img2pose的網(wǎng)絡(luò)上。若跑超分模型,由于超分模型速度很快,所以性能下降不算多,只下降了6fps,說明超分不是很大的負(fù)擔(dān)。然后我們嘗試跑兩路流水線,觀察是否會(huì)對GPU有進(jìn)一步的性能壓榨,結(jié)果是否定的,性能基本沒有改變,A10和T4的前后數(shù)據(jù)基本相同,說明一路流水線已經(jīng)比較充分地使用了資源。

既然Img2pose的網(wǎng)絡(luò)是最大的性能瓶頸,那我們來對其進(jìn)行進(jìn)一步的分析。測試的環(huán)境與之前相同,我們選用了兩張照片,其中一張是大合影,里面有52張人臉,另一張是自拍,里面只有一張人臉。做完NMS后得到的值如表格中所示,后處理不包括NMS,可以看到后處理是很快的,基本可以被忽略。NMS就稍微慢一些,最初,原作者在PyTorch的代碼里使用的是CPU上的NMS,但這個(gè)數(shù)據(jù)測出來不穩(wěn)定,表格中展示的是最好情況的數(shù)據(jù),有時(shí)候數(shù)據(jù)可能會(huì)增長到五十多或六十多,并且在人臉較多的情況下,還會(huì)變得更慢,這個(gè)問題就很嚴(yán)重。在GPU上的NMS的時(shí)間就很穩(wěn)定,都為2ms。另外,使用原作者的PyTorch的代碼跑52張人臉需要三百多毫秒,與跑單張人臉?biāo)钑r(shí)間相比,性能慢了很多,但經(jīng)過我們的一系列加速后,二者最后都穩(wěn)定在了40ms。我們也測試了其他照片,無論照片中有多少張人臉,時(shí)間基本都為40ms,這也符合我們對算法、模型的預(yù)期。

另外一個(gè)模型是3DDFA,這個(gè)模型比較簡單,對其的分析也比較簡單。測試的環(huán)境與之前大致相同,不一樣的是3DDFA模型使用TensorRT加速,TensorRT直接就能跑onnx。中間處理部分(使用的是python代碼)使用CUDA kernel重構(gòu),使其能跑在GPU上。另外,里面還有OpenCV的操作,OpenCV的操作是在CPU上的,但我們展示的是沒有使用CV-CUDA下的性能,這是因?yàn)槟壳癈V-CUDA尚未開源,在GitHub上開源的3DDFA管線是未使用CV-CUDA的版本,所以O(shè)penCV的操作并沒有經(jīng)過GPU加速,還是CPU的版本。

觀察圖中表格,當(dāng)不跑超分模型時(shí),性能比img2pose快很多,A10上能跑到160。但有個(gè)奇怪的地方是,A2理論上只有T4的一半,但結(jié)果卻是A2跑得比T4還快。這其實(shí)與CPU有關(guān),如剛才所說,很多OpenCV的操作在CPU上,A2這個(gè)服務(wù)器的CPU比較好,所以它跑得比較快,這也說明這個(gè)模型目前的瓶頸完全不在GPU上,必須趕快把CPU上的操作移植出去,才能更充分地利用GPU。同時(shí)也可以看到,跑兩路會(huì)有一個(gè)更好的效果,這也說明此時(shí)并沒有把GPU用完。

06 未來展望

現(xiàn)在已經(jīng)介紹完設(shè)計(jì)和性能分析的內(nèi)容,最后跟大家分享一下我們現(xiàn)在正在做的工作和未來展望。

我們目前做的一個(gè)工作叫做自由流水線,它跳出了FFmpeg框架。因?yàn)槲覀円庾R(shí)到FFmpeg框架有很多的限制,業(yè)界的一些客戶也對此很不滿,所以他們會(huì)自己寫轉(zhuǎn)碼的框架或進(jìn)程,這樣就不需要遵循FFmpeg的條條框框,調(diào)用推理、cv的op就會(huì)很方便。FFmpeg專業(yè)且強(qiáng)大,但在云渲染場景存在限制,比如有些客戶跟我們提到,他們的視頻來源很單一,可以完全控制視頻格式的數(shù)量(比如只有264和265),且分辨率和碼率也是有限的組合。那么,由于無需應(yīng)對很多轉(zhuǎn)碼格式,就不需要非常復(fù)雜的處理轉(zhuǎn)碼的能力,可以直接調(diào)用FFmpeg的API,而不是使用FFmpeg的binary,故即使客戶自己寫的轉(zhuǎn)碼的APP不是非常專業(yè),但也能滿足需求。

另外,F(xiàn)Fmpeg還有其他的限制。當(dāng)前通過啟用多個(gè)FFmpeg進(jìn)程來跑多路,即跑一路轉(zhuǎn)碼就啟用一個(gè)FFmpeg進(jìn)程,這樣若某一路轉(zhuǎn)碼失敗,其他的轉(zhuǎn)碼還能繼續(xù)跑,彼此間不會(huì)相互影響,但這在GPU上會(huì)造成限制,具體原因如下:CUDA Context是按進(jìn)程計(jì)算的,只有打開MPS時(shí),多個(gè)進(jìn)程才能共享一個(gè)CUDA Context,若不打開MPS,那每個(gè)進(jìn)程會(huì)有一個(gè)自己的CUDA Context,一個(gè)系統(tǒng)內(nèi)就會(huì)有多個(gè)CUDA Context共存,若多個(gè)CUDA Context都指向同一個(gè)GPU,多個(gè)CUDA Context只能對GPU時(shí)分復(fù)用(這種情況和一個(gè)單核心單物理線程的CPU一樣,多線程只能時(shí)分復(fù)用,每個(gè)線程都分別跑一個(gè)時(shí)間片),假設(shè)我要跑60路轉(zhuǎn)碼、推理,過程就是第一路上去跑一會(huì)兒再下來,然后第二路上去跑一會(huì)兒再下來,依此類推,雖然單純的轉(zhuǎn)碼或硬件轉(zhuǎn)碼器不會(huì)受到CUDA Context的限制,但計(jì)算和渲染會(huì)受到影響,因?yàn)橛?jì)算“來回跑”的方式?jīng)]有辦法實(shí)現(xiàn)真正的任務(wù)級并行的效果,而是在時(shí)分復(fù)用,這是串行而不是并行。

此外,雖然大家覺得FFmpeg簡單一些,但FFmpeg GPU filter開發(fā)流程比較復(fù)雜,這從之前的介紹內(nèi)容就可以看出,我們實(shí)際在做的時(shí)候也踩了不少“坑”,關(guān)鍵是文檔少信息少,遇到問題時(shí)只能自己調(diào)試、摸索。

綜上所述,我們想做自由流水線,是想將我們團(tuán)隊(duì)這些年在編解碼、圖像處理、AI推理領(lǐng)域積累的內(nèi)容整合成一個(gè)框架并對外開源(這個(gè)就是我之前提到的要做的大的版本更新),這個(gè)框架是一個(gè)op的合集和不同場景下的sample合集,大家可以根據(jù)這些op快速搭建出進(jìn)程或程序。此外,我們也會(huì)提供python的binding,大家可以直接在python里調(diào)用框架來快速做一個(gè)流水線、demo。具體地,這個(gè)框架包含視頻/圖片編解碼,常見cv算子,以及經(jīng)典場景的示例。

我們已經(jīng)使用新的框架加速了3DDFA v2模型,整體的感受就是其更加靈活、限制更少,這是因?yàn)闊o需封裝進(jìn)libavfilter。其次,打batch更方便,在FFmpeg中filter打batch非常麻煩,需要自己攢幀,比如要打成batch等于4,就需要來一幀攢一幀,直到攢夠四幀才能進(jìn)行一次推理或渲染,因此在FFmpeg中打batch是不夠方便的,但在自己寫的程序里可以隨便拼接數(shù)據(jù),所以更方便。然后,我們使用了CV-CUDA做加速,在A10上單路720p視頻吞吐可達(dá)220fps,之前單路只可達(dá)160fps,性能得到了很大的提升。

剛才提到了場景的sample,我們現(xiàn)在還在做GPU的HEIF/HEIC圖片編解碼。HEIF格式是一個(gè)圖片的容器,將圖片或圖片序列使用H.265(HEVC)編碼,并將其裝入HEIF容器。H.265的壓縮率優(yōu)于JPEG,而且其可做圖片序列,可做動(dòng)圖,支持無損。由于是硬件編碼,故其吞吐高,在圖靈上實(shí)測編碼1080p靜態(tài)HEIF圖像吞吐可達(dá)400fps(包括了容器打包的時(shí)間)。另外,由于是單獨(dú)的硬件加速,故不占用CUDA core,減少了圖片編碼對推理或渲染的影響。

我們還做了視頻抽幀。在審核等其他場景下無法也無需做到對視頻逐幀推理,只需根據(jù)某種固定的時(shí)間間隔抽取視頻中的某些幀,然后只處理這些有代表性的幀即可。間隔可以是時(shí)間間隔也可以是幀數(shù)間隔,比如一秒鐘抽取兩幀或者隔三幀抽取一幀。此外,可將抽出的幀編碼為HEIF/JPEG落盤。

最后和大家談一下未來的展望,跟大家探討一下我們看到的東西。未來AI+Graphics的場景會(huì)十分多樣且定制化,我們今天介紹的開發(fā)內(nèi)容大部分都是定制化的,尤其是渲染的filter,需要根據(jù)實(shí)際內(nèi)容思考如何做軟件。之前提到,基于FFmpeg很難滿足所有場景,所以我們在探索新的形式。

另外,GPU的利用存在門檻,軟件不夠豐富,我們希望進(jìn)一步提供更加豐富的工具和軟件生態(tài),讓大家在各種層次上更加便捷地利用GPU。

同時(shí),我們還遇到了一些問題,比如數(shù)字人的數(shù)字資產(chǎn)在商業(yè)的引擎里,渲染也全在UE里完成,但UE不是庫,所以不能直接被調(diào)用,UE執(zhí)行分發(fā)時(shí),其打包好的內(nèi)容是獨(dú)立的可執(zhí)行文件,那么推理如何和打包好的渲染的程序交互呢?一般是通過跨進(jìn)程、跨節(jié)點(diǎn)通信完成的,但實(shí)現(xiàn)起來會(huì)存在一些問題,并且有些客戶自研的引擎針對的是渲染場景,沒有圖形接口,與我們之前探討的內(nèi)容不一樣,針對這些問題我們正在探索解決。

此外,未來ARM在數(shù)據(jù)中心的占比會(huì)越來越高,因?yàn)锳RM會(huì)提供更高的計(jì)算性價(jià)比,若想做高質(zhì)量的渲染或編碼,那么CPU軟件編碼要比硬件編碼好,各個(gè)廠家都有非常厲害的CPU軟件編碼的實(shí)現(xiàn),所以在對渲染和編碼的質(zhì)量要求極高的場景下,我們考慮實(shí)現(xiàn)GPU計(jì)算/渲染與CPU軟件編碼的組合。

最后,現(xiàn)在的模型越做越大,芯片之間需要互相通信,互相傳輸數(shù)據(jù)。有些客戶希望渲染和計(jì)算部署到不同的節(jié)點(diǎn),我們之前的形式就滿足不了這樣的要求,因?yàn)槲覀儗⒂?jì)算和渲染放在了同一個(gè)filter,難以實(shí)現(xiàn)跨節(jié)點(diǎn)的要求。客戶期望的這種方式的好處是,管理、調(diào)度方便,這對運(yùn)維是有益的,但對技術(shù)實(shí)現(xiàn)有更高的要求,需要更靈活的部署和伸縮能力。

未來,我們會(huì)持續(xù)開發(fā)項(xiàng)目,添加更多的目標(biāo)場景和功能,繼續(xù)開發(fā)生態(tài),歡迎大家積極參與討論和建設(shè)!

以上就是本次分享的所有內(nèi)容,謝謝大家!

    本文為澎湃號(hào)作者或機(jī)構(gòu)在澎湃新聞上傳并發(fā)布,僅代表該作者或機(jī)構(gòu)觀點(diǎn),不代表澎湃新聞的觀點(diǎn)或立場,澎湃新聞僅提供信息發(fā)布平臺(tái)。申請澎湃號(hào)請用電腦訪問http://renzheng.thepaper.cn。

            查看更多

            掃碼下載澎湃新聞客戶端

            滬ICP備14003370號(hào)

            滬公網(wǎng)安備31010602000299號(hào)

            互聯(lián)網(wǎng)新聞信息服務(wù)許可證:31120170006

            增值電信業(yè)務(wù)經(jīng)營許可證:滬B2-2017116

            ? 2014-2026 上海東方報(bào)業(yè)有限公司