Kissat学习笔记

Kissat学习笔记

前言

SAT(Boolean Satisfiability Problem)是一个NP完全问题,在IC前端设计中,SAT验证是一个重要环节,它要求判定一个布尔公式是否存在一组变量赋值使其为真,于是在十几年间诞生了许多高效的SAT求解器。Kissat求解器曾在SAT竞赛中取得了优异成绩,作为CaDiCal求解器的继承者,Kissat在保持高性能的同时,通过优化内存和简化代码实现了更高的效率。

原理

Kissat采用CDCL(Conflict-Driven Clause Learning)框架,它的启发式算法是其高效的核心涵盖变量选择、阶段选择和冲突分析等多个方面。研究表明,它通过以下方式优化搜索过程:

  1. 变量和阶段选择:
    • 目标阶段:Kissat 使用目标阶段来指导变量的赋值,特别是在可满足实例上表现良好。这是从 CaDiCaL 移植过来的技术 (System Description Paper for SAT Competition 2020)。
    • 模式切换:求解器在稳定模式和聚焦模式之间动态切换,基于“滴答”(缓存行访问计数)来测量传播过程中的进展,而不是运行时间。 (CaDiCaL, Gimsatul, IsaSAT and Kissat Entering the SAT Competition 2024)。
  2. 冲突分析和决策控制:
    • 动态glue限制计算:Kissat 根据子句使用统计计算第 1 层和第 2 层胶水限制。第 1 层胶水对应 50% 使用子句的机会(50% 的机会被使用),第 2 层胶水对应 90% 使用子句的机会(<10% 的机会被使用)。初始限制为胶水 2(第 1 层)和胶水 6(第 2 层) (CaDiCaL, Gimsatul, IsaSAT and Kissat Entering the SAT Competition 2024)。这实际上可以让求解器决定保留哪些子句以优化搜索。
    • glue提升:在冲突分析期间定期更新单个子句的胶水值,确保子句的实用性保持最新 (CaDiCaL, Gimsatul, IsaSAT and Kissat Entering the SAT Competition 2024, http://ijcai.org/Proceedings/09/Papers/074.pdf)。
    • 二元原因跳转:引入以减少冲突分析时间,当cnf>100,000 个子句且>99% 二元子句启用 (CaDiCaL, Gimsatul, IsaSAT and Kissat Entering the SAT Competition 2024)。
    • 滴答用于决策率控制:当决策率(冲突之间的决策)过高时,平衡聚焦和稳定模式 (CaDiCaL, Gimsatul, IsaSAT and Kissat Entering the SAT Competition 2024)。
  3. 阶段选择:
    • 幸运阶段检测与 SLUR 预览:从 CaDiCaL 移植,扩展了 SLUR 预览,作为初始预处理的一部分 (CaDiCaL, Gimsatul, IsaSAT and Kissat Entering the SAT Competition 2024)。

此外,Kissat 内部采用“变量移动到前部启发式”进行决策,这意味着频繁参与冲突的变量被优先考虑。

子句管理的详细机制

Kissat 的子句管理是其高效性能的另一个关键,优化了存储、分类和操作。

内联二元子句:二元子句内联到watching堆栈中,与 CaDiCaL 相比,减少了观察大小,从 16 字节(CaDiCaL)减少到 4 字节(二元)和 8 字节(大型子句,由于阻塞文字) (System Description Paper for SAT Competition 2020)。

以下是子句管理的一些关键参数的表格:

文字处理
  • 文字编码方案:(literal.h)

    • 正负文字编码:
      • 假设变量索引为 n
      • 正文字编码为: 2n
      • 负文字编码为: 2n + 1
    • 编码特性:
      • 同一变量的正负文字只有最低位不同
      • 右移1位(IDX)可以获得变量索引
      • 左移1位(LIT)可以从索引获得正文字
      • 异或1(NOT)可以在正负文字间转换
    • 不变性保证
      • 索引保持
      • 正负关系保持

    eg:

    变量索引 5:

    • 正文字 = 5 << 1 = 10
    • 负文字 = 10 + 1 = 11
    • IDX(10) = 10 >> 1 = 5
    • IDX(11) = 11 >> 1 = 5

src

main.c
  • 功能:

    1. 程序入口:接收命令行参数,初始化SAT求解器
    2. 信号处理:处理用户中断、超时信号,并确保程序正常终止时能够打印统计信息
    3. 资源管理:初始化和释放内存,有防止内存泄漏措施,还有调试模式下的额外检查
  • 主要函数:

    int main (int argc, char **argv) {
      int res;
      solver = kissat_init ();
      kissat_init_alarm (kissat_alarm_handler); 
      kissat_init_signal_handler (kissat_signal_handler);
      res = kissat_application (solver, argc, argv); //运行求解器
      kissat_reset_signal_handler (); //重置信号处理
      ignore_alarm = true; //忽略剩余的定时器信号
      kissat_reset_alarm ();
      kissat_release (solver);
    #ifndef NDEBUG
      if (!res)
        return kissat_dump (0); //调试模式下内存泄漏检查
    #endif
      return res;
    }
    

    从main函数可以看出kissat程序运行的主要部分是res = kissat_application (solver, argc, argv); //运行求解器,所以下一步我们来看application.c文件。

application.c
  • 功能

    是求解器的应用层入口,在此文件处理了命令行交互、文件读写、结果输出、资源管理

  • 主要结构体

    struct application {
      kissat *solver;
      const char *input_path; //输入文件路径
      const char *output_path; //输出文件路径
    #ifndef NPROOFS
      const char *proof_path; //证明文件路径
      file proof_file;
      int binary;
    #endif
    #if !defined(NPROOFS) || !defined(KISSAT_HAS_COMPRESSION)
      bool force;
    #endif
      int time; //时间限制
      int conflicts; //冲突次数限制
      int decisions; //决策次数限制
      strictness strict; //解析严格程度
      bool partial; //是否部分赋值
      bool witness; //是否输出解
      int max_var;
    };
    
  • 主要函数

    • static bool parse_options (application *application, int argc, char **argv)  //处理命令行选项,-h:帮助,-f:强制写证明,-n:不输出满足解,-q:屏蔽所有消息,-s:输出统计信息,-v:增加详细信息
      
    • 输入处理

      static bool parse_input (application *application)
      

      读取DIMACS格式的输入文件

    • 程序运行

      static int run_application (kissat *solver, int argc, char **argv,bool *cancel_alarm_ptr)
      

      在此函数中主要进行初始化、解析命令行选、读取输入文件、kissat_solve (solver);求解SAT问题、输出结果、输出统计信息和清理资源,由此来看kissat_solve (solver);函数所在的internal.c文件

internal.c
  • 功能

    此文件是kissat的内部实现文件,包含了求解器的核心功能

    • 内存管理
      • 动态分配和释放内存
      • 跟踪内存泄漏
    • 求解控制
      • 设置决策限制
      • 设置冲突限制
      • 终止控制
    • 字句处理
      • 添加字句
      • 处理重复文字
      • 单元传播
    • 变量管理
      • 变量导入导出
      • 变量激活/失活
      • 变量值查询
  • 主要结构

    在看函数之前,首先要了解kissat求解器的核心结构,在internal.h文件中定义了大量的结构,总的来说,这个头文件定义了求解器的数据结构、核心功能接口、辅助宏定义、状态管理机制,那么我们先来看看这些重要的结构吧:

    • kissat,这是最主要的结构

      struct kissat {
          // 1. 状态标志
          bool stable;          // 稳定模式标志
          bool watching;        // 是否使用监视文字
          bool preprocessing;   // 预处理阶段标志
          
          // 2. 核心计数器
          unsigned vars;        // 变量数
          unsigned size;       // 规模
          unsigned active;     // 活跃变量数
          unsigned level;      // 当前决策层
          
          // 3. 主要数组
          value *values;       // 变量的值
          flags *flags;        // 变量标记
          watches *watches;    // 监视文字列表
          
          // 4. 搜索相关
          heap scores;         // 变量评分堆
          double scinc;       // 分数增量
          queue queue;        // 决策变量队列
          
          // 5. 学习子句管理
          reference last_learned[4];    // 最近学习的子句
          arena arena;                 // 子句存储区
      }
      
    • 重要宏定义

      // 变量和文字数量
      #define VARS (solver->vars)
      #define LITS (2 * solver->vars)
      
      // 遍历宏
      #define all_variables(IDX) \
          unsigned IDX = 0, IDX##_END = solver->vars; \
          IDX != IDX##_END; \
          ++IDX
      
      #define all_literals(LIT) \
          unsigned LIT = 0, LIT##_END = LITS; \
          LIT != LIT##_END; \
          ++LIT
      

    在kissat结构体中还有很多子结构,我们在后面遇到了再说。现在先看回internal.c文件。

  • 主要函数

    • 初始化函数
    kissat *kissat_init (void) {
        kissat *solver = kissat_calloc (0, 1, sizeof *solver); //分配内存
        kissat_init_options (&solver->options); // 初始化选项
        kissat_init_profiles (&solver->profiles); // 初始化配置文件
        kissat_init_queue (solver); // 初始化队列
        solver->watching = true;
        solver->conflict.size = 2;
        solver->scinc = 1.0;
        solver->first_reducible = INVALID_REF;
        solver->last_irredundant = INVALID_REF;// 设置默认值
        return solver;
    }
    
    • 资源释放函数

      void kissat_release (kissat *solver)
      
    • 变量预留

      void kissat_reserve (kissat *solver, int max_var) {
          // 预分配变量空间
          // 导入文字
          // 激活文字
      }
      
    • 选项设置

      int kissat_set_option (kissat *solver, const char *name, int new_value);
      int kissat_get_option (kissat *solver, const char *name);
      
    • 限制设置

      void kissat_set_decision_limit (kissat *solver, unsigned limit);
      void kissat_set_conflict_limit (kissat *solver, unsigned limit);
      
    • 字句添加

      void kissat_add (kissat *solver, int elit) {
          // 添加文字到求解器
          // 处理重复文字
          // 处理单元子句
          // 处理空子句
      }
      
    • kissat_solve求解函数

      int kissat_solve (kissat *solver) {
        // 1. 检查求解器是否已初始化
        kissat_require_initialized (solver);
        
        // 2. 确保当前子句栈为空
        kissat_require (EMPTY_STACK (solver->clause),
                        "incomplete clause (terminating zero not added)");
        
        // 3. 确保不是增量求解模式
        kissat_require (!GET (searches), "incremental solving not supported");
        
        // 4. 调用主搜索函数
        return kissat_search (solver);
      }
      

      可以看到这个函数主要还是一个前置条件检查的功能,当满足上述前三点条件才开始调用搜索函数,对于其返回值有如下含义:

      #define SATISFIABLE 10 //可满足SAT
      #define UNSATISFIABLE 20 //不可满足UNSAT
      #define UNKNOWN 0 //未知
      

      那么我们接着来看kissat_search(solver)函数所在文件search.c

search.c

此文件夹主要的函数是kissat_search,也是真正kissat求解器的核心实现,具体实现过程是:

  1. 单元传播
  2. 冲突分析
  3. 字句学习
  4. 变量决策
  5. 优化处理

以此来实现高效传播。

  • 主要函数

    int kissat_search (kissat *solver) // 返回值: 10(SAT), 20(UNSAT), 0(UNKNOWN)
    
    • 初始化

      if (solver->inconsistent)
          res = 20;                    // 已经发现不可满足
      if (!res && GET_OPTION (luckyearly))
          res = kissat_lucky (solver); // 尝试幸运求解
      if (!res && kissat_preprocessing (solver))
          res = kissat_preprocess (solver); // 预处理
      
    • 主要搜索循环

      while (!res) {
          // 1. 单元传播
          clause *conflict = kissat_search_propagate (solver);
          
          // 2. 处理冲突
          if (conflict)
              res = kissat_analyze (solver, conflict);
          
          // 3. 检查终止条件
          else if (!solver->unassigned)
              res = 10;    // 所有变量已赋值,找到解
              
          // 4. 其他操作
          else if (kissat_reducing(solver))         // 子句删除
              res = kissat_reduce(solver);
          else if (kissat_switching_search_mode(solver))  // 切换搜索模式
              kissat_switch_search_mode(solver);
          else if (kissat_restarting(solver))       // 重启
              kissat_restart(solver);
          else if (kissat_reordering(solver))       // 变量重排序
              kissat_reorder(solver);
          else
              kissat_decide(solver);                // 做决策
      }
      

      搜索支持两种搜索模式:stable、focused模式;优化技术通过子句学习和删除、变量重排序、重新设置变量相位等来实现。

至此,kissat的运行流程就是这样,接下来就是要深入挖掘它是如何具体实现的,可以从以下5个方面着手(后面有时间再写):

  • kissat_search_propagate: 单元传播
  • kissat_analyze: 冲突分析
  • kissat_reduce: 子句删除
  • kissat_reorder: 变量重排序
  • kissat_decide: 变量决策

你可能感兴趣的:(IC设计,c语言,算法,启发式算法)