跳转至

Blog

Gem5工作梳理

几条主线 1. C906/C920配置建模 2. RISC-V、xuantie扩展指令与寄存器 3. Systemc/tlm 协同仿真

总览

随着现在的计算系统越来越复杂,越来越异构,gem5作为单个建模CPU的平台有些不够用,虽然它的精度不错。systemc作为一个C++用于建模的库,也有很多不错的开源项目,比如Dramsys,或是自己写的仿真单元。那么能不能把二者结合到一起,使得既能利用gem5的高精度CPU,其他地方又可以高度自定义来实现整系统的仿真?

答案是可以的,gem5很早便提供了相关代码。为了把systemc和gem5协同仿真,需要解决几个核心问题。(为了方便,以下用sc简称systemc) 1. 事件调度队列统一。gem5和sc都是事件调度仿真,可以视作都有一个调度内核,同时还都有一个事件队列,维护类似这样的队列,当二者到一起时,这个事件队列是由一方维护还是二者一起,如果二者一起,两边事件怎么做到同步? 2. 全局时钟/计时方式统一:gem5计时方式是tick,一个tick=1ps,而sc也有计时方式,是秒,协同仿真时,两边都有计时,但是对于全局的负载,只有一个时间,此时看谁的? 3. port通信时,如何转换:协同仿真最重要的就是gem5和sc的通信,通信一般通过port,gem5内部有自己的port类型,sc内部也有sc port以及tlm通信,各自传递的packet格式不同,port也不同,如何绑定port以及正确在两种世界传递包?

tlm是一种通信协议,广泛用于sc,详细了解可以看这篇blog,https://www.cnblogs.com/sasasatori/p/19077607

port通信简介

为了后续说明如何协同仿真方便一点,在此补充一点背景知识。一般来说两个不同模块通信需要通过port,然后发送方传递包,接收方接受并处理,然后加上处理延时,处理完成之后回复。比如CPU给cache发送访存请求就是通过这种方式。

一般的gem5 port通信流程

port分为request和response port,需要实现不同的接口

class RequestPort {
  ...
  public:
    bool sendTimingReq(PacketPtr pkt);
    // inherited from TimingRequestProtocol in `src/mem/protocol/timing.hh`
    virtual bool recvTimingResp(PacketPtr pkt) = 0;
    virtual void sendRetryResp();
   ...
};

class ResponsePort {
  ...
  public:
    bool sendTimingResp(PacketPtr pkt);
    // inherited from TimingResponseProtocol in `src/mem/protocol/timing.hh`
    virtual bool recvTimingReq(PacketPtr pkt) = 0;
    virtual void sendRetryReq();
   ...
};

gem5每个port会有一个peer,这个peer,在端口通信时port本身会调用peer的对应函数 - sendTimingReq 最终调用 peer->recvTimingReq(pkt) - sendTimingResp 最终调用 peer->recvTimingResp(pkt) - 通过调用peer注册函数的返回值,判断request或者response是否成功,如果不成功需要retry

sc的TLM2.0通信

在sc建模中一般都使用TLM2.0通信,分为blocking和non-blocking,详见前面推荐的blog,下图是non-blocking,有4个phase

加入bridge的port通信流程

如果要把二者结合起来,那么需要一个中介/bridge,因为两边通信协议不同,对于gem5 to tlm来说,gem5的sendtimingreq应该对应TLM的两个REQ阶段,TLM的BEGIN_RESP则会调用gem5的sendtimingresp

这个bridge其实做了很多事情: 1. 将gem5的packet转为tlm的generic payload,二者的内容差不多但是需要转换一下数据类型 2. 一侧通过gem5的port bind绑定到gem5,另一侧通过sc的port bind绑定到sc 3. 将sc侧返回的事件插入gem5事件调度队列,比如当前这次req用时,resp用时,只是记录用于通信的事件,其他sc内部处理的事件由sc自己插入eventq

协同仿真需要做到的事情在文章最开始提及了,主要是事件调度队列统一或者不统一但是要严格按照事件预期发生时间执行,然后是计时方式统一,然后是通过port bind实现函数调用,以及正确初始化二者,因为仿真是只有一个入口,所以通常会在一个入口(gem5/sc)初始化到另一方。下面具体介绍两种sc各自实现上述特性的区别。

gem5提供的systemc

gem5内部提供的systemc大致分为两种,第一种**native systemc**是gem5将sc内核适配到gem5,修改了很多,比如事件调度都采用gem5内部的event,计时方式也是直接采用gem5的tick;而第二种**ext systemc**基于2.3.1版本,基本没改。

上面二者的编译方式、计时方式也有区别,不过原理大概相同,都会通过一个transactor,以及通过port bind将sc的socket绑定到transactor上面,下面简单说明一下

普通 gem5:main() -> instantiate -> simulate() * 因此需要把sc也加入到这里的初始化 Ext systemc:sc_main() -> SystemC scheduler -> Module::eventLoop() -> gem5 event queue * 需要把gem5通过sc来调用其初始化函数,并且作为sc的一个thread由sc内核管理

下面是二者主要区别:

程序入口 事件调度 主时钟 debug手段
Native systemc gem5 main gem5为主 gem5 curtick 齐全,有debug flag与性能计数器
Ext systemc sc_main sc为主,gem5维护自己的eventq sc_time_stamp 只有sc自己的,原debug flag和性能计数器需要适配

对原版systemc所做的修改 Native systemc

其实这里是最核心的地方,你会看到怎么把sc的内核挂到gem5的eventq,以及怎么在二者之间转移控制权,并且维护一个统一的eventq,sc的eventq虽然是挂到gem5上面,但是需要sc内核自己处理,不过本文重点不在这上面,在了解基础原理并且使用

  • sc_main不是由sc内核驱动,通过ScMainFibergem5 的控制流里执行
  • sc_start()sc_pause()sc_stop() 最终都落到 sc_gem5::scheduler
  • sc_time_stamp() 直接读取 scheduler.getCurTick()
  • sc的各个phase映射到gem5的eventq
    • initialization phase
    • evaluate phase
    • update phase
    • delta notification phase
    • timed notification phase
  • ...

Ext systemc - 移除了 SystemC 对 bundled Boost 的依赖 - 把相关调用替换为 C++11 STL

事件调度区别

native sc

  1. 事件队列统一:SystemC_Kernel 构造时把 scheduler 绑定到 gem5 eventq ```c++ Kernel::Kernel(const Params &params, int) :

gem5::SimObject(params),

t0Event(*this, false, gem5::EventBase::Default_Pri - 1)

{

// Install ourselves as the scheduler's event manager.

::sc_gem5::scheduler.setEventQueue(eventQueue());

} ``` 2. notify和wait等延时通知最终挂到gem5 scheduler:所有sc notify或者wait会调用gem5自己的schedule函数,往自己内部的eventq插入事件,而不会插入到sc内核的eventq,重点看下面的schedule

void
Event::notify(const sc_core::sc_time &t)
{
    if (delayedNotify.scheduled()) {
        if (scheduler.delayed(t) >= delayedNotify.when())
            return;

        scheduler.deschedule(&delayedNotify);
    }
    scheduler.schedule(&delayedNotify, t);
}

  1. 计时统一:sc time stamp也是修改过,直接获取gem5的curtick获取当前tick

这样就能够实现,1是全局只有一个eventq,2是全局时钟同步,以gem5 to sc为例,程序都是单线程,当一个包发到sc时,控制权转移到sc,sc内部可以进行很多处理,只需要正常插入事件队列即可。

来看个例子:recvTimingReq,假设是一个从gem5到tlm的请求,一般是通过回来的payload得到delay,然后将 '现在的时间'+delay = 事件预期发生时间,插入到gem5的事件队列中

调用到bridge的recvtimingreq->fw会有不同返回值

status = socket->nb_transport_fw(*trans, phase, delay);
1. 如果返回tlm::accepted会先挂起packet等到sc处理完req通过nb_transport_bw插入事件队列
template <unsigned int BITWIDTH>
tlm::tlm_sync_enum
Gem5ToTlmBridge<BITWIDTH>::nb_transport_bw(tlm::tlm_generic_payload &trans,
    tlm::tlm_phase &phase, sc_core::sc_time &delay)
{
    auto cb = [this, &trans, phase]() { pec(trans, phase); };
    auto event = new EventFunctionWrapper(
            cb, "pec", true, getPriorityOfTlmPhase(phase));
    system->schedule(event, curTick() + delay.value());
    return tlm::TLM_ACCEPTED;
}

2.返回TLM_UPDATED
    ...
    } else if (status == tlm::TLM_UPDATED) {
    // The Timing annotation must be honored:
    sc_assert(phase == tlm::END_REQ || phase == tlm::BEGIN_RESP);
    // Accepted but is now blocking until END_REQ (exclusion rule).
    blockingRequest = trans;
    packetMap.emplace(trans, packet);
    auto cb = [this, trans, phase]() { pec(*trans, phase); };
    auto event = new EventFunctionWrapper(
            cb, "pec", true, getPriorityOfTlmPhase(phase));
    system->schedule(event, curTick() + delay.value());

  1. gem5 当前某个事件运行到 bridge,调用 recvTimingReq()
  2. bridge 立刻同步调用 target 的 nb_transport_fw(),这一步已经进入 SC module 代码,但还不是 scheduler 驱动的 SC process 执行。
  3. target 在 nb_transport_fw() 里调用 peq.notify(trans, phase, delay)
  4. peq.notify(...) 最终调用 sc_event.notify(delay)
  5. sc_event.notify(delay) 会通过 scheduler.schedule(&delayedNotify, t) 把这个延时事件挂到 gem5 eventq
  6. 当前 gem5 事件执行完,控制权回到 gem5 主仿真循环,doSimLoop() 下一轮继续从 eventq 取队头
  7. 当 eventq 跑到这个 SC 定时点,会执行对应 TimeSlot,TimeSlot::process() 把这一时刻所有 SC timed events 跑掉
  8. timed event 触发 m_e,peq 里那个敏感的 method process 被唤醒,变成 ready,peq_with_cb_and_phase 构造时就 spawn_method() 了一个 fec
  9. scheduler 安排 readyEvent
  10. eventq 跑到 readyEvent 时,runReady() 才真正执行这个 fec() method fec() 再调用 owner callback,也就是 C920SimpleMemoryTarget::peq_cb(...)
  11. peq_cb() 里可能再 peq.notify(END_REQ, acceptDelay)、再 peq.notify(BEGIN_RESP, responseLatency())
  12. 当 target 通过 nb_transport_bw() 把 phase 回给 bridge 时,bridge 再把这个 phase 用 gem5 event 安排在 curTick()+delay

ext systemc

这种方式,是把gem5作为一个库,然后sc链接库,调用库内提供的接口进行gem5 simobj的初始化;入口是sc_main,可以把gem5理解为sc的一个thread/method

因此除了bridge之外,还需要有实现gem5嵌入到sc的文件 gem5_within_systemc:把libgem5嵌入到sc进程,目的是 - SystemC 作为总调度器 - gem5 作为被托管的仿真内核 - gem5 的 config.ini 通过 CxxConfigManager 在 C++ 里构造 - gem5 事件循环、日志、统计、异步事件都能在 SystemC 环境里工作

对于gem5来说,sc专门为他新建了一个sc的eventq,然后gem5内部通过schedule,往这个eventq里面维护事件,当然由于这个eventq和sc一般的eventq不一样,时间是tick,需要特殊处理,因此注册了sc method进行处理

void

Module::setupEventQueues(Module &module)

{

fatal_if(gem5::mainEventQueue.size() != 0,

"Gem5SystemC::Module::setupEventQueues must be called"

" before any gem5 event queues are set up");



gem5::numMainEventQueues = 1;

gem5::mainEventQueue.push_back(new SCEventQueue("events", module));

gem5::curEventQueue(gem5::getEventQueue(0));

}

eventloop处理eventq

SC_METHOD(eventLoop);
sensitive << eventLoopEnterEvent;
dont_initialize();

SC_METHOD(serviceExternalEvent);
sensitive << externalSchedulingEvent;
dont_initialize();

通过一个sc thread run进入处理,然后执行simulate,在这里触发eventloop这个敏感事件,同时wait(exit_event),敏感事件会触发eventloop回调函数,然后从eventq里面取出事件进行执行,此时会比较sc time和下一个事件预计发生time,分别进行不同的处理,注意可能同一tick对应多个事件同时执行,这也对应了sc中的delta cycle语义,等到exit event检测到了,就退出仿真。

sc可能有多个eventq,但都由sc内核统一管理,由他来维护时间上各个事件从前到后执行的时间正确性。

gem5的**这个event queue谁来维护? gem5自己来维护,通过schedule插入**,在gem5 within systemc这条路径中

  gem5::numMainEventQueues = 1;
  gem5::mainEventQueue.push_back(new SCEventQueue("events", module));
  gem5::curEventQueue(gem5::getEventQueue(0));

大致流程: 1. sc_main() 创建 SimControl 或 Gem5TopLevelModule 2. SimControl 构造时 setupEventQueues(),把 gem5 顶层 event queue 换成 SCEventQueue 3. sc_start() 后,SC_THREAD(run) 调 simulate() 4. simulate() 触发 eventLoop() 5. eventLoop() 用 catchup() 把 gem5 时间追到 sc_time_stamp() 6. 如果 gem5 下个事件在未来,就在 SystemC 里 notify(wait_period) 并返回 7. 到时间后 SystemC 再次唤醒 eventLoop() 8. 如此往复,直到 gem5 产生退出事件

port bind区别

  1. native sc:gem5 port bind流程:
  2. Tlm port:重写了上面的port.bind函数,使得bind不仅绑定tlm的端口,还绑定gem5的端口,gem5端口绑定其实不是绑定,只是设置一下peer指针
  3. Tlm的port需要通过wrapper包一层,然后override gem5_getport来返回这个port,实现连接
  4. initiator需要注册bw,此注册由gem5_to_tlm中的before_end_of_elaboration来负责
  5. target需要注册fw,target自己负责

  6. ext sc:

接入sc时,sc这一方需要做什么?

对于外部sc来说,他会实例化util/tlm里的包装类,这些包装类会调用libgem5库,外部sc可见的高层接口是 - Gem5SimControl:顶层控制器,负责读 config.ini、注册 tlm_master/tlm_slave handler、建立 event queue、开统计/调试、最后调用 simulate() 跑 gem5 - Gem5MasterTransactor:TLM target socket,把外部 TLM 请求送进 gem5 - Gem5SlaveTransactor:TLM initiator socket,把 gem5 请求送到外部 TLM 世界。 - Gem5SimControlInterface:给 transactor 绑定用的最小接口,只有 getSlavePort() / getMasterPort()。 - SCMasterPort / SCSlavePort:桥的 gem5 侧,分别继承 gem5::ExternalMaster::ExternalPort / gem5::ExternalSlave::ExternalPort,负责 gem5 packet 和 TLM payload 互转。

sc_main()内实例化上述对象

参考

gem5论文

lab1

直接用unicode不太行,词表太大,有的词也很稀疏。使用unicode encoding,一般使用UTF-8,可以表示大多数 encoding后的每个byte都是0~255,但是一个字可能由多个byte组成

但是这样有个问题,一个word本来只用一个编码就能表示,utf8需要多个这会造成计算、数据依赖更加复杂 折中:subword tokenization,作为二者的tradeoff

比如一个word,“the”,经常出现,assigning it an entry in the vocabulary would reduce this 3-token sequence to a single token. BPE:a compression algorithm that iteratively replaces (“merges”) the most frequent pair of bytes with a single, new unused index. 把最常使用的bytes块,用一个没用过的词汇表index代替。

词汇表项,要么是byte,要么是合并的字节序列,tradeoff

BPE training的步骤 1. vocabulary initialization:词汇表其实就是,bytestring到id的一一映射。由于utf8有256个可能的字节值,我们的初始词汇表大小为256。 2. pre-tokenization:简单来说bpe就是统计上一步词汇表的各种byte,出现的次数,然后就可以merge * 但是这样计算量太大。并且在语料库中直接合并字节可能会导致标记仅在标点符号上有所不同(例如,dog! vs.dog.)。这些标记将获得完全不同的标记ID,即使它们可能具有高语义相似性(因为它们仅在标点符号上有所不同)。 * 解决:pre tokenization,可以看做对语料库的粗粒度统计,用一些常见的word,先简单训练一遍BPE,比如“text”,可以把相邻的t,e都增加10次出现的次数。最简单的办法就是以空格分割,每个word粗粒度统计 3. compute BPE merges:最终的词汇表大小是256+ number of merged BPE word 还有special token需要保留,不能拆分,初始化时即可加入,比如<|endoftext|> 如果多个pair出现的频率都是最高,prefer字典序更大的合并

例子:

一次次迭代,每次merge最高频率的

换句话说,特殊标记在训练期间定义硬分割边界,但它们本身不应该对合并计数做出贡献。

加速,不要每次merge一对,再计算,这样每次改变的其实就只有频率最高的对,其他的都没变,下次迭代重复计算太多。因此,可以通过索引所有对的计数并增量地更新这些计数来提高BPE训练速度,而不是显式地迭代每对字节来计算对频率。类似cache?

试着使用profiling找出程序瓶颈

Transformer

参考资料:https://zhuanlan.zhihu.com/p/219714713

overview

首先,我们将Transformer看成一个简单的黑盒模型。在机器翻译任务下,将源语言(法语)的一个句子A输入其中,产生一个目标语言(英语)的句子B。 有多少个encoder取决于你的超参数,图中是6个,但是没有什么特殊意义。

所有的encoder结构是一样的,但是不共享权重,每个encoder结构如下 首先经过一个self-attention层(该层可以帮助encoder在对特定单词进行编码时查看输入句子中的其他单词),然后是一个前馈神经网络(feed-forward neural network,ffnn)ffnn主要用于对每个token自身的特征进行变换

decoder的结构类似,但是在self-attention和ffnn之间多了一个attention层:

encoder

第一个encoder会有embedding层,用于把词转为向量 encoder的self-attention层各个word是有依赖的,不能并行?但是feed forward中各个word是独立的,可以并行

self-attention

  1. 对每一个输入向量计算三个向量,计算方式是与三个不同矩阵乘
    • q:queries
    • k:keys
    • v:values
  2. 对某一word进行encoding时,需要计算该单词和其他单词的注意力分数:该单词的 query 点乘**另一单词的 key 矩阵**
  3. 将分数除以维度开根
  4. 然后softmax
  5. 当前计算token的 value 乘 softmax分数,保留关注词的value,削弱非相关词的value
  6. 该word相对于每个其他word经过第5步都会有一个结果,需要对对value加权求和形成一个,这也就是encoder的结果 总结为一个公式: 例子:

多头注意力

该word与多个word组成的子空间的关系

position encoding

表示token在句子中的位置,一般是由一个position encoding向量与embedding之后的向量直接相加

残差结构

encoder和decoder中的每个子层(Self-Attention,ffnn)在其周围都有残差连接与层归一化(layer normalization)操作 可视化 这也是用于decoder的子层

decoder

encoder算出k,v矩阵之后就完成职责了,这些k,v会被decoder每次反复使用

最后的线性层和softmax

主要目的是把浮点向量变为一个词 * 线性层是一个简单的完全连接的神经网络,它将解码器堆栈产生的向量投影到一个更大的向量中,称为logits向量。 * softmax层将这些分数转换为概率(全部为正,全部相加为1.0)。 选择具有最高概率的单元,然后该单元对应的单词将作为该时间步的输出。

Systemc TLM2.0 AT

参考: 1. https://www.cnblogs.com/sasasatori/p/19077607 2. https://ywinh.github.io/show-gem5-sc/21

tlm_generic_payload

private:

/* --------------------------------------------------------------------- */

/* Generic Payload attributes: */

/* --------------------------------------------------------------------- */

/* - m_command : Type of transaction. Three values supported: */

/* - TLM_WRITE_COMMAND */

/* - TLM_READ_COMMAND */

/* - TLM_IGNORE_COMMAND */

/* - m_address : Transaction base address (byte-addressing). */

/* - m_data : When m_command = TLM_WRITE_COMMAND contains a */

/* pointer to the data to be written in the target.*/

/* When m_command = TLM_READ_COMMAND contains a */

/* pointer where to copy the data read from the */

/* target. */

/* - m_length : Total number of bytes of the transaction. */

/* - m_response_status : This attribute indicates whether an error has */

/* occurred during the transaction. */

/* Values supported are: */

/* - TLM_OK_RESP */

/* - TLM_INCOMPLETE_RESP */

/* - TLM_GENERIC_ERROR_RESP */

/* - TLM_ADDRESS_ERROR_RESP */

/* - TLM_COMMAND_ERROR_RESP */

/* - TLM_BURST_ERROR_RESP */

/* - TLM_BYTE_ENABLE_ERROR_RESP */

/* */

/* - m_byte_enable : It can be used to create burst transfers where */

/* the address increment between each beat is greater */

/* than the word length of each beat, or to place */

/* words in selected byte lanes of a bus. */

/* - m_byte_enable_length : For a read or a write command, the target */

/* interpret the byte enable length attribute as the */

/* number of elements in the bytes enable array. */

/* - m_streaming_width : */

/* --------------------------------------------------------------------- */



sc_dt::uint64 m_address;

tlm_command m_command;

unsigned char *m_data;

unsigned int m_length;

tlm_response_status m_response_status;

bool m_dmi;

unsigned char *m_byte_enable;

unsigned int m_byte_enable_length;

unsigned int m_streaming_width;

tlm_gp_option m_gp_option;

阻塞:需要b_transport * 注册 * bind:initiator绑定target

非阻塞:需要nb_transport * 注册 通过 register_nb_transport_[fw|bw] 在对应的port * initiator注册bw * target注册fw * bind:

m_init.m_initiator_port.bind(m_target.m_target_port);
四个phase:

fw和bw都是处理收到的phase fw: * BeginREQ * END_RESP

bw函数内:每个case会调用一个回调函数 * END_REQ * BEGIN_RESP

begin放入,即notify 而end消费,即get

peq_with_get:特殊的工具,用于管理信息传输过程中的时序(使用了tlm提供的peq_with_get类)

gem5 load elf

gem5的memory还有一类是system memory,用来load elf等 根因不是汇编指令本身,而是你的自定义 SystemC memory 路径没有给 SE mode 提供一个可用的 AbstractMemory/physmem。

关键点有两个:

  1. System.memories 只会收集 AbstractMemory 子对象,src/sim/System.py (line 64) 明确写的是 VectorParam.AbstractMemory(Self.all, ...)。
    但你这条链路里的两个对象:

    • C920TlmPrintMem.py (line 7)
    • TlmBridge.py (line 35) 里的 Gem5ToTlmBridgeBase

    都是 SystemC_ScModule,不是 AbstractMemory。所以它们不会进入 System.memories。

  2. System 在构造时会用 p.memories 去建 physmem,src/sim/system.cc (line 167)。然后 SEWorkload::setSystem() 会从 sys->getPhysMem().getConfAddrRanges() 取地址范围,填充 memPools,src/sim/se_workload.cc (line 43)。
    如果 System.memories 是空的,那 getConfAddrRanges() 就是空,memPools.populate() 后 pools 仍然是空的。

后面一到装载 ELF 的阶段就会炸:

  • Process::initState() 会先用 SETranslatingPortProxy(Always) 把 ELF 段写进目标内存,src/sim/process.cc (line 300)。
  • MemoryImage::write() 会对每个 segment 做 writeBlob/memsetBlob,src/base/loader/memory_image.cc (line 38)。
  • 一旦目标页还没映射,SETranslatingPortProxy::fixupRange() 就会调用 process->allocateMem(),src/mem/se_translating_port_proxy.cc (line 55)。
  • allocateMem() 再去调用 seWorkload->allocPhysPages(),src/sim/process.cc (line 317)。
  • 最后 MemPools::allocPhysPages() 直接做 pools[pool_id].allocate(npages),src/sim/mem_pool.cc (line 162),但这时 pools 是空的,所以是越界访问,结果就是你看到的 segfault。

所以这个崩溃发生在“程序还没真正开始执行 _start/main 之前”,是装载器在给进程分配物理页时死掉的。
你那个 systemc_mem.out 的 readelf 也能佐证这点:它有一个 RW LOAD segment,MemSiz 是 0x2002810,装载器会在 tick 0 之前就去为它分配并清零很多页

gem5会调用functional request来load image,路径如下,接到tlm时,最终会调用到transport_dbg 你要的完整路径,在当前 SE + configs/c920 下是这条:

  1. Process::initState() 创建 SETranslatingPortProxy(tc, Always),然后调用 image.write(*initVirtMem) 见 src/sim/process.cc:289
  2. MemoryImage::write() 遍历 segment,调用 proxy.writeBlob(seg.base, seg.data, seg.size) 见 src/base/loader/memory_image.cc:38
  3. proxy 的动态类型是 SETranslatingPortProxy,所以 PortProxy::writeBlob() 内部调用的虚函数 tryWriteBlob(),实际落到 TranslatingPortProxy::tryWriteBlob() 见 src/mem/port_proxy.hh:192 和 src/mem/translating_port_proxy.cc:101
  4. TranslatingPortProxy::tryWriteBlob() 先做 translateFunctional() 把虚拟地址翻成物理地址,然后调用 PortProxy::writeBlobPhys(range.paddr, ...) 见 src/mem/translating_port_proxy.cc:101
  5. PortProxy::writeBlobPhys() 构造一个 WriteReq 的 Packet,然后执行 sendFunctional(&pkt) 见 src/mem/port_proxy.cc:75
  6. 这个 sendFunctional 在 PortProxy(ThreadContext *tc, ...) 构造时,已经绑成了 tc->sendFunctional(pkt) 这个 lambda 见 src/mem/port_proxy.cc:47
  7. ThreadContext::sendFunctional() 取当前 CPU 的 getDataPort(),然后调用这个 RequestPort 的 sendFunctional(pkt) 见 src/cpu/thread_context.cc:157
  8. RequestPort::sendFunctional() 调 FunctionalRequestProtocol::send(_responsePort, pkt) 见 src/mem/port.hh:579
  9. FunctionalRequestProtocol::send() 最终就是直接调用对端 peer->recvFunctional(pkt) 见 src/mem/protocol/functional.cc:48
  10. 在当前 configs/c920 拓扑里,这个 functional 包会沿着已绑定的端口链往下走: CPU data port -> L1D -> L2 bus/L2 -> membus -> memory port 原因是:

  11. CPU dcache 连到 L1D:见 configs/c920/cache_hierarchy.py:96

  12. L1D 再连到 L2 bus:见 configs/c920/cache_hierarchy.py:100
  13. memory side 最终挂到 board.get_mem_ports() 返回的 port:见 configs/c920/cache_hierarchy.py:75
  14. 对于 systemc_memory,这个 port 就是 bridge.gem5:见 configs/c920/systemc_memory.py:45 和 configs/c920/systemc_memory.py:60

  15. 所以最后到达 Gem5ToTlmBridge::recvFunctional(),桥再调用 socket->transport_dbg() 进 SystemC 见 src/systemc/tlm_bridge/gem5_to_tlm.cc:501

peq

peq_with_get 和 peq_with_cb_and_phase 区别和联系

对比simple mem和sc mem

  • Bus/XBar 给每个包打上 headerDelay=2ns、payloadDelay=3ns,见 src/mem/xbar.cc:118 和 src/mem/xbar.cc:134
  • 请求大小都是 64B
  • bandwidth 对应一次请求占用 5ns
  • latency=30ns
  • A/B/C 是 3 个不同 cache line 的 LLC miss
  • A@0ns,B@1ns,C@3ns

  • LLC -> Bus -> SimpleMemory

代码落点在 src/mem/simple_mem.cc:144、src/mem/simple_mem.cc:154、src/mem/simple_mem.cc:174。

Time LLC Bus SimpleMemory ---- ---------------- --------------------- ----------------------------- 0ns miss A ---------> packet(A,h=2,p=3) → recvTimingReq(A) accept A immediately busy = [0, 5] resp_ready(A) = 0+2+3+30 = 35

1ns miss B ---------> packet(B,h=2,p=3) → recvTimingReq(B) busy, return false B stays upstream (still in LLC/bus side)

3ns miss C ---------> packet(C,h=2,p=3) → recvTimingReq(C) busy, return false C also stays upstream

5ns release() sendRetryReq()

5ns retry B --------> packet(B,h=2,p=3) → recvTimingReq(B) accept B busy = [5, 10] resp_ready(B) = 5+2+3+30 = 40

10ns release() sendRetryReq()

10ns retry C --------> packet(C,h=2,p=3) → recvTimingReq(C) accept C busy = [10, 15] resp_ready(C) = 10+2+3+30 = 45

这个路径里,拥塞点就在 SimpleMemory 入口。 A 忙的时候,B/C 都卡在 memory 上游,没法下沉。

  1. LLC -> Bus -> Transactor -> SC SimpleMemory

代码落点在:

  • bridge 用 headerDelay 作为 BEGIN_REQ 的注入延迟,并清掉 payloadDelay,见 src/systemc/tlm_bridge/gem5_to_tlm.cc:421
  • bridge 在收到 END_REQ 时才对 gem5 侧 sendRetryReq(),见 src/systemc/tlm_bridge/gem5_to_tlm.cc:223
  • target 用 busyUntil 和 payloadDuration() 推迟 END_REQ,见 src/systemc/tlm_bridge/c920_tlm_simple_mem.cc:83、src/systemc/tlm_bridge/c920_tlm_simple_mem.cc:87
  • target 在 END_REQ 之后再等 responseLatency() 发 BEGIN_RESP,见 src/systemc/tlm_bridge/c920_tlm_simple_mem.cc:93

Time LLC Bus Bridge/Transactor SC SimpleMem ---- ----------- -------------- ----------------------------- ------------------------- 0ns miss A ---> pkt(A,h=2,p=3) recvTimingReq(A) blockingRequest = A BEGIN_REQ(A) scheduled at 2ns -

1ns miss B ---> pkt(B,h=2,p=3) bridge still blocked by A return false for B -

2ns BEGIN_REQ(A) -----------------> target sees A acceptDelay = 0 busyUntil = 2+5 = 7 END_REQ(A) at 2ns 2ns <---------------- END_REQ(A) unblock request channel sendRetryReq() for B BEGIN_RESP(A) at 32ns

2ns retry B → pkt(B,h=2,p=3) recvTimingReq(B) blockingRequest = B BEGIN_REQ(B) scheduled at 4ns -

3ns miss C ---> pkt(C,h=2,p=3) bridge blocked by B return false for C -

4ns BEGIN_REQ(B) -----------------> target sees B busyUntil=7 > 4 acceptDelay = 3 END_REQ(B) at 7ns busyUntil = 12 7ns <---------------- END_REQ(B) 9ns BEGIN_REQ(C) -----------------> target sees C busyUntil=12 > 9 acceptDelay = 3 END_REQ(C) at 12ns BEGIN_RESP(C) at 42ns

图里最关键的不同

可以把两条路径压缩成一句话:

  • SimpleMemory:A 在服务时,B/C 都卡在 memory 上游
  • Bridge + SC:A 在服务时,B 已经可以更早地下沉到 bridge/target 之间,只剩 C 卡在上游

也就是:

SimpleMemory 拥塞时: [LLC/MSHR/bus侧] B, C | [memory内] A

Bridge+SC 拥塞时: [LLC/MSHR/bus侧] C | [bridge/target等待槽] B | [SC memory内] A

这相当于 Bridge + SC 比 SimpleMemory 多了一个“请求暂存槽”。

为什么这会导致 miss / MSHR / prefetch 统计不同

  • 对不同 cache line 的 miss: B/C 更早地下沉到 LLC 以下,所以更早占用下游独立资源。结果通常是: mshrHits 变少,no_mshrs 变多。
  • 对同一 cache line,或者 prefetch 和 demand: 由于桥这边 payloadDelay 没像 SimpleMemory 那样直接算进 resp_ready,响应可能更早回来。上面例子里: A 在 SC 路径是 32ns,在 SimpleMemory 是 35ns。 这 3ns 足够让“prefetch 先填进去”还是“demand 先到 miss”发生翻转,所以: pfLate、pfHitInMSHR、甚至 demandMisses 都会变。

simple mem是按照duration为节点发下一个req,req节点为[0,duration1,duration1+duration2...] sc mem则是按照END_REQ为节点发下一个req [0,head1,.. 公式比较复杂

gem5 处理 riscv 中断

一般的 RISCV 中断+任务切换

告诉硬件“当前 hart 可以暂停,等到中断可能需要服务时再继续”的指令。
但它不保证一定休眠,甚至实现成 NOP 也合法;因此软件在 WFI 后必须自己检查是否真的有需要处理的事件

一般的中断来源,中断发起,中断处理

1 中断发起,在 hart 这一侧,硬件会体现为 mip(machine interrup pending) 里的某些 pending 位有效,比如: - mip.MSIP - mip.MTIP - mip.MEIP - 这里的关键是:中断请求先到达hart并变成pending,不等于立刻跳去中断处理函数

2 核心在指令边界检查能否响应该中断: 处理器通常在指令提交边界检查:

  • 全局中断使能是否开了:mstatus.MIE
  • 该类中断是否单独使能:mie 对应位
  • 当前特权级是否允许被这个级别的中断打断
  • 是否被委托到更低级模式:mideleg / sideleg(裸机通常不配,默认都进 M 模式)
  • 如果同时有多个 pending,中断控制器/架构优先级规则决定先接哪个

中断是异步的,但通常是在指令之间被接收

中断响应,hart允许接收中断之后,会做如下 * 设置中断返回地址,mepc = pc+4 * 把中断原因写入mcause * 更新mstatus * MPIE = MIE * MIE = 0 (关中断) * MPP = 之前的特权级 * pc = mtvec(中断设置的trap地址) * mtvec 有两种常见模式:

  • Direct:所有 trap 都跳到 BASE
  • Vectored:中断跳到 BASE + 4 * cause,异常还是跳 BASE

硬件不会帮你做的事情: * 不会自动保存通用寄存器x1~x31 * 不会自动切换栈 * 不会恢复寄存器 * 中断处理完成之后不会执行mret

mret时硬件的动作: - pc <= mepc - MIE <= MPIE - 当前特权级恢复为 MPP - MPIE/MPP 按架构规则复位到返回后的状态

Eda实践课

DC

启动dc shell:dc_shell 查看report:report_xx

topo模式加入了对后端的一些估计布局布线 到dct/tmp

dc_shell -topo | tee -i dct.log

普通 DC:逻辑综合 + 统计线延迟。
DC Topo:逻辑综合 + 物理感知 + 更准的线延迟 + 更接近后端的优化。

dct/output_data 找 ./scan 三个文件

解包icc1.tar

粘贴回

gem5访存梳理

https://github.com/orgs/gem5/discussions/1687

以O3CPU+classic cache为例子,梳理cacheable和non-cacheable的访存顺序

需要关注的是 CPU -> LSQ -> Cache(mshr、L1、L2) -> DDR cacheable和non-cacheable路径的区别 乱序访存,乱的是什么部分,non-cacheable可以乱吗?

大概主线: IEW dispatch/execute -> O3 LSQ(loadQueue/storeQueue + LSQRequest) -> LSQ DcachePort -> L1 Cache(cpu_side_port) -> L1 MSHR / writeBuffer -> coherent xbar -> L2 Cache(cpu_side_port) -> L2 MSHR / writeBuffer -> coherent xbar / memory <- response 按原路返回 <- L2 先处理自己的 MSHR <- L1 再处理自己的 MSHR <- LSQRequest 完成,指令 writeback/commit

CPU-> LSQ

通过dispatch阶段指令分发,(此时已经是乱序的了?)识别到 load 或者 store指令,就把对应的指令放到 load 或者 store queue里面等待发射

此处的store处理有些特殊,只是把数据放到store queue,等到该条指令commit时,才能发到cache,这是因为: 1. load 要拿回数据,结果会立刻影响后续依赖指令,所以它必须尽早发出。 2. store 不需要从内存拿回普通结果,它真正的架构副作用是“修改内存”,这件事不能在提交前对外发生。 3. O3 需要精确异常和可回滚性,所以 store 先留在 storeQueue,等 commit 之后再慢慢往 cache/memory 排出;load 则可以投机发出,错了再 squash 掉响应。 这个store queue有点类似store buffer,他会做store-to-load forwarding 区分write_buffer,wb位于cache,不在cpu,是用来排队向下层发writeback/clean,evict/uncacheable write这类包

等待一会儿资源就绪或者执行单元有空了,则会执行load/store ldstQueue.executeLoad(inst) -> LSQUnit::executeLoad()

CPU会通过buildPackets创建一个包然后发出去

LSQ->Cache

lsq::read 会进行 store-load forwarding

uncacheable read:LSQ ->loadqueue -> L1 MSHR -> L2 MSHR -> response 1. L1收到lsq的请求之后,在Cache::access中处理,如果是uncacheable,会把本级可能已有的同地址line flush 2. 无视已有mshr,强制分配一个新的mshr,强制miss,挂在mshr queue,等待发出,不创建cacheline 3. L2收到之后进行同样的流程: * Cache::access() 看到 UC,invalidate 本级 line,强制 miss - handleTimingReqMiss() 为 UC read 新分配 L2 自己的 MSHR - 以后再由 L2 的 sendMSHRQueuePacket() 往更下层发 1. 收到responce之后一路返回,由于这是 isForward 的 uncacheable read,is_fill 为假,所以 L1和L2不会 fill,只会 serviceMSHRTargets() 把数据交给它保存的 target(也就是来自 L1 的那个请求包)

uncacheable write:LSQ -> storequeue -> L1 writeBuffer -> L2 writeBuffer -> response write需要等到commit时才会发出请求 * L1收到后,不走mshr而是走write_buffer,区别在于writeBuffer entry 在成功发出后就释放了,它不是等 ack 的地方。

write_buffer和mshr同时都ready,派谁出去;sendDeferredPacket -> getNextQueueEntry,后者会进行仲裁,每次只选一个queue entry - 如果 wq_entry ready,并且 writeBuffer.isFull(),或者当前没有 ready 的 miss_mshr,就优先尝试发 writeBuffer。 - 但发 writeBuffer 之前,会先查 mshrQueue.findPending(wq_entry);如果有同地址冲突、而且那个 miss 更早(order < wq_entry->order),就先发那个 MSHR。 - 否则,如果有 miss_mshr ready,默认发 MSHR。 - 但发 MSHR 前,还会查 writeBuffer.findPending(miss_mshr);如果有冲突的 pending write,就先发这个 writeBuffer entry,代码注释说是为了先保住 dirty data。

gem5如何区分cacheable和uncacheable * riscv通过PMAchecker

MMIO和uncacheable的区别? MMIO一般需要是强序的,不能推测执行,退休时才能执行 MMIO属于uncacheable,他更多代表一种设备类型

load/store * uncacheable * strict order MMIO一般是uncacheable && stirct order

uncacheable load 如果不是 strictly ordered,执行时就能发;如果是 strictly ordered,则要等它到 ROB 头、由 commit 触发 non-spec replay 后才发。它不是“退休完成以后”才发,而是“到 commit 点、在真正退休前发”。

解释清楚,为什么cacheable和uncacheable的store路径不同

gem5建模c920

config对齐rtl与920手册

性能测试

测试方式主要是和硬件跑相同的elf,硬件需要配置玄铁相关csr来enable 920的特性,保证基本特征如prefetch等对齐 然后硬件需要配置,以dump性能计数器。与gem5 dump出的stats.txt进行对比

参数太多了,主要对比什么参数?

集成的两条路

ext systemc

gem5 systemc

集成编译

在nbu_model/cmake/下增加了gem5.cmake

需要enable cxx_std_17,因为gem5编译时依赖这个选项,不然运行时会报错

有几个target 1. gem5_tlm

由于原本nbu model build时依赖的lib fmt、yaml-cpp、systmec在旧的g++工具链编译的,而worker22的工具链比较新,导致link时出现abi不兼容,gem5还只能在这个worker编译

因此从外网github导入了对应版本的包,在worker22重新编译,放到了/home/common_share/SMG/gem5_newlib下面

需要更改path.cmake改成对应的path才能编译成功

blocking inst fifo设计

背景:ks2硬件提出的新方案,所有的nscl指令会通过四个sd来拼接,64bits*4 = 256,写到inst fifo结构,该fifo负责暂存各个64bits数据以及整合发送256bits拼接指令

术语解释: * payload:指tlm2.0的标准包数据结构,tlm_generic_payload

对外接口 1. 一个slave tlm2.0接口,由gem5 bridge连接 2. 一个master tlm1.0接口,连接到instr queue

数据结构设计 * 内部核心结构是四个fifo,分别是fifo 0~3,每个fifo深度为4,每个entry是64bits * 其余比较重要的是一个sc event,当4个fifo都有效时触发pop 256bits

fifo构造时需要一个base addr和size,目前和硬件对齐在构造时写死,base addr是0x0a082000,size是16*8 = 128bytes 当前设计四个fifo,每个fifo深度为4,每个entry是64bits

fifo什么时候加入entry,通过 gem5的内存请求 -> bridge -> 转换为tlm2.0 nb请求 -> blocking请求 -> instrfifo::b_transport_bus * gem5有三种仿真mode(timing、atomic、blocking)由配置文件指定,一般都用timing模式建模比较精确,但是timing模式对应的其实是tlm2.0的nb transport,我只实现了b transport,是如何正确调用的? * systemc帮忙做的,如果没有实现nb,会把nb的语义转为blocking,从而正确调用

当请求到来时行为 1. assert是write请求,fifo只能被gem5写 2. 从payload提取addr,check addr在规定范围之内,以及data length为8 bytes,若失败assert 0 3. 从payload copy data 4. 根据addr来确定往哪个fifo push(地址映射,规则和robin的图一致) 5. push时采用sc fifo的阻塞式write,如果当前fifo满了,是会在这里一直卡住,等到有空闲的entry 6. 成功push之后,设置response ok 7. 如果当前push到fifo之后,4个fifo能够拼接一个256bits,则直接notify pop事件,pop 256

fifo什么时候出entry * pop thread一直在wait一个事件,该事件由push fifo在4个fifo都有有效entry时触发 * 采用fifo.read,阻塞式读出消耗entry * 定义一个数据结构接收:内部是一个uint64数组,长度为4,接受4个fifo读出的值,然后调用decode模块生成fullinstptr,通过tlm1.0 port发送给instr queue

TODO:延时设计

讨论过两种入fifo的设计 Sc fifo是否是有四个就触发?还是说要第四个进来时才从reg写到fifo 即0 1 2 3可触发,0 2 3触发时发现不对不够,后来了个1,如何处理? 后续看rtl check

目前的设计是第一种有4个就出发,提前写入fifo,没有经过reg

256bits decode

背景:之前nbu一直都是64bits decode,即直接接收指令本身,然后需要解码出一个高级数据结构fullinstr供instr queue驱动。ks2不再接收指令本身,而是利用编译器,将每条原本64bits的nscl转为4条sd,4*64=256bits,因此decoder需要解析256bits指令

区别: * 64 bits指令本身,其中寄存器field存的是寄存器index,而不是具体值,因此一般的decode流程是

根据pc从mem中读出64bits指令,然后decode生成fullinstr,只不过由于指令中只有寄存器index,没有具体值,此时各个reg的val还是0 需要额外通过一个fill register步骤从mem中读出值,填写到生成的fullinstr数据结构中,此时才算构造好可以传给下一模块处理

现在256bits,其实自带了寄存器值,不用一个额外的步骤从mem中读取寄存器值,而是直接从256里面读,然后填充fullinstr,传给下一模块

decode256的实现 该函数的输入为拼接好的256bits,uint64_t array[4] * 借鉴硬件的实现,有一个helper函数,提取256bits中每个寄存器的start,end bits * 然后有一个extract 256函数,该函数接收start,end,然后从256数据结构中提取对应值。需要解决跨64bits的获取值问题,因为虽然是256,但是是以64为单位,从数组中索引的,如果是跨64,就需要索引两个entry(我们把数组看作是4个entry) * 读取出值之后,在makeinstr时,把寄存器值也填进去,index现在默认0,因为256bits中是没有存储index信息的,但是可以知道name,如果一定要index,可以通过某种方式索引name得到index,比如一个预先填好的固定的map

简单来说,256和64的不同之处,就在于没有index了,需要通过顺序读取某条指令的所有寄存器,记录start和end bit

最前面提取低12位,低12位都是一样的,然后逐个提取reg 位域

问题:格式可能不太优雅,和之前的没有对齐

遇到一个bug rtl和页面上decode的起始bit不一样,导致decode识别错误,修改start bit后解决

transactor接入,单ccu/双ccu

ddr和fifo都接入

此时其实有两种设计,一种是gem5只走一个transactor,每个transactor其实是要对应到一个addr列表,这样gem5才能够知道如果访问到了这里的地址时,会发送到transactor而不是内部的某个mem,如果只采用一个transactor,那么也就只有一个addr列表,那么这里需要两串地址,[[fifo],[ddr]。gem5做的只是识别到对指定addr的load/store时转发到transactor,然后transactor通过一个socket连接到sc bus,由sc bus来进行具体地址的区分和转发,什么地方转发到fifo,什么地址转发到ddr?

第二种设计是采用两个transactor,分别对应fifo和ddr,这样的好处是不用建一级bus,集成比较快速和方便

load elf方案: 方案1. gem5来load elf到ddr 可能需要tlm2.0的direct mem ptr来初始化ddr,需要转到transactor上

方案2.sc来load elf到ddr 方案2改动会少一点,sc暴露一个接口让gem5拿到pc就好,对以后的host也方便,兼容现有的一些别的功能

可以不load elf,但是可以解析elf,这样不需要sc通过某种方式传pc 需要确认gem5的load elf和nbu 的load elf功能是否有区别

debug手段

遗留的问题

  1. gem5的uncacheable目前会通过cache层次,会加上tag lookup+read/write resp延迟,可以考虑更改,如果影响大的话